json-fs-query 2.3.0 → 3.0.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.
Files changed (58) hide show
  1. package/README.md +82 -9
  2. package/browser/jsonFsQuery.js +486 -209
  3. package/browser/jsonFsQuery.min.js +1 -1
  4. package/dist/data/queries/groupBy.d.ts +20 -1
  5. package/dist/data/queries/groupBy.js +71 -9
  6. package/dist/data/queries/groupBy.min.js +1 -1
  7. package/dist/data/queries/limitOffset.d.ts +1 -1
  8. package/dist/data/queries/limitOffset.js +1 -1
  9. package/dist/data/queries/selectFields.d.ts +6 -4
  10. package/dist/data/queries/selectFields.js +15 -10
  11. package/dist/data/queries/selectFields.min.js +1 -1
  12. package/dist/data/utilities/parsers/findCommand.d.ts +2 -1
  13. package/dist/data/utilities/parsers/findCommand.js +5 -5
  14. package/dist/data/utilities/parsers/findCommand.min.js +1 -1
  15. package/dist/data/utilities/parsers/findConditions.d.ts +8 -9
  16. package/dist/data/utilities/parsers/findConditions.js +29 -41
  17. package/dist/data/utilities/parsers/findConditions.min.js +1 -1
  18. package/dist/data/utilities/parsers/findGroupBy.d.ts +6 -4
  19. package/dist/data/utilities/parsers/findGroupBy.js +17 -9
  20. package/dist/data/utilities/parsers/findGroupBy.min.js +1 -1
  21. package/dist/data/utilities/parsers/findInsertValues.d.ts +4 -3
  22. package/dist/data/utilities/parsers/findInsertValues.js +9 -9
  23. package/dist/data/utilities/parsers/findInsertValues.min.js +1 -1
  24. package/dist/data/utilities/parsers/findJoinClauses.d.ts +4 -3
  25. package/dist/data/utilities/parsers/findJoinClauses.js +20 -13
  26. package/dist/data/utilities/parsers/findJoinClauses.min.js +1 -1
  27. package/dist/data/utilities/parsers/findJoinEntity.d.ts +1 -1
  28. package/dist/data/utilities/parsers/findJoinEntity.js +3 -5
  29. package/dist/data/utilities/parsers/findJoinEntity.min.js +1 -1
  30. package/dist/data/utilities/parsers/findLimit.d.ts +4 -3
  31. package/dist/data/utilities/parsers/findLimit.js +8 -8
  32. package/dist/data/utilities/parsers/findLimit.min.js +1 -1
  33. package/dist/data/utilities/parsers/findMergeJoins.d.ts +4 -3
  34. package/dist/data/utilities/parsers/findMergeJoins.js +19 -12
  35. package/dist/data/utilities/parsers/findMergeJoins.min.js +1 -1
  36. package/dist/data/utilities/parsers/findOffset.d.ts +4 -3
  37. package/dist/data/utilities/parsers/findOffset.js +8 -8
  38. package/dist/data/utilities/parsers/findOffset.min.js +1 -1
  39. package/dist/data/utilities/parsers/findSelects.d.ts +4 -3
  40. package/dist/data/utilities/parsers/findSelects.js +13 -14
  41. package/dist/data/utilities/parsers/findSelects.min.js +1 -1
  42. package/dist/data/utilities/parsers/findSortClauses.d.ts +4 -3
  43. package/dist/data/utilities/parsers/findSortClauses.js +12 -12
  44. package/dist/data/utilities/parsers/findSortClauses.min.js +1 -1
  45. package/dist/data/utilities/parsers/findUpdateValues.d.ts +5 -7
  46. package/dist/data/utilities/parsers/findUpdateValues.js +10 -13
  47. package/dist/data/utilities/parsers/findUpdateValues.min.js +1 -1
  48. package/dist/data/utilities/parsers/parseQuery.js +83 -9
  49. package/dist/data/utilities/parsers/parseQuery.min.js +1 -1
  50. package/dist/data/utilities/parsers/splitClauses.d.ts +29 -0
  51. package/dist/data/utilities/parsers/splitClauses.js +139 -0
  52. package/dist/data/utilities/parsers/splitClauses.min.js +1 -0
  53. package/dist/data/utilities/runQuery.js +5 -4
  54. package/dist/data/utilities/runQuery.min.js +1 -1
  55. package/package.json +1 -1
  56. package/dist/data/utilities/parsers/removeFromQuery.d.ts +0 -2
  57. package/dist/data/utilities/parsers/removeFromQuery.js +0 -8
  58. package/dist/data/utilities/parsers/removeFromQuery.min.js +0 -1
@@ -1,2 +1,21 @@
1
- export declare const groupBy: (dataSet?: any[], groupProperty?: string) => any;
1
+ /**
2
+ * One group of a grouped read: the field and the value its rows share, then either the rows themselves or, for
3
+ * `group by a, b`, the groups within it by the next field. A group of rows missing the field has `missing: true`
4
+ * and no value; `null` is a value of its own (see Null and missing fields in the README).
5
+ */
6
+ export type Group = {
7
+ field: string;
8
+ value?: any;
9
+ missing?: true;
10
+ rows?: Object[];
11
+ groups?: Group[];
12
+ };
13
+ /**
14
+ * Group rows by each field in turn. The groups are a list in the order their first row came, so a sort before
15
+ * grouping orders the groups too (and the rows within each one).
16
+ * @param dataSet
17
+ * @param fields
18
+ * @returns The rows as they were when there's no field, otherwise the groups.
19
+ */
20
+ export declare const groupBy: (dataSet?: Object[], fields?: string[]) => Object[] | Group[];
2
21
  //# sourceMappingURL=groupBy.d.ts.map
@@ -4,19 +4,81 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.groupBy = void 0;
7
+ require("core-js/modules/es.json.stringify.js");
7
8
  require("core-js/modules/esnext.iterator.constructor.js");
9
+ require("core-js/modules/esnext.iterator.map.js");
8
10
  require("core-js/modules/esnext.iterator.reduce.js");
9
- const groupBy = (dataSet = [], groupProperty = '') => {
10
- if (!groupProperty) {
11
+ require("core-js/modules/esnext.map.delete-all.js");
12
+ require("core-js/modules/esnext.map.every.js");
13
+ require("core-js/modules/esnext.map.filter.js");
14
+ require("core-js/modules/esnext.map.find.js");
15
+ require("core-js/modules/esnext.map.find-key.js");
16
+ require("core-js/modules/esnext.map.includes.js");
17
+ require("core-js/modules/esnext.map.key-of.js");
18
+ require("core-js/modules/esnext.map.map-keys.js");
19
+ require("core-js/modules/esnext.map.map-values.js");
20
+ require("core-js/modules/esnext.map.merge.js");
21
+ require("core-js/modules/esnext.map.reduce.js");
22
+ require("core-js/modules/esnext.map.some.js");
23
+ require("core-js/modules/esnext.map.update.js");
24
+ // An object's keys in sorted order, at any depth, so { a, b } and { b, a } are the same value; arrays keep their order
25
+ const sortedKeys = value => {
26
+ if (Array.isArray(value)) {
27
+ return value.map(sortedKeys);
28
+ }
29
+ if (value !== null && typeof value === 'object') {
30
+ return Object.keys(value).sort().reduce((sorted, key) => {
31
+ sorted[key] = sortedKeys(value[key]);
32
+ return sorted;
33
+ }, {});
34
+ }
35
+ return value;
36
+ };
37
+ // Rows share a group when their values are the same, type included: 1 and '1' are two groups, as are null and missing
38
+ const groupKey = (row, field) => {
39
+ if (!Object.prototype.hasOwnProperty.call(row, field)) {
40
+ return 'missing';
41
+ }
42
+ const value = row[field];
43
+ return value !== null && typeof value === 'object' ? `json:${JSON.stringify(sortedKeys(value))}` : `${typeof value}:${String(value)}`;
44
+ };
45
+ /**
46
+ * Group rows by each field in turn. The groups are a list in the order their first row came, so a sort before
47
+ * grouping orders the groups too (and the rows within each one).
48
+ * @param dataSet
49
+ * @param fields
50
+ * @returns The rows as they were when there's no field, otherwise the groups.
51
+ */
52
+ const groupBy = (dataSet = [], fields = []) => {
53
+ if (!fields.length) {
11
54
  return dataSet;
12
55
  }
13
- return dataSet.reduce((groupedData, data) => {
14
- const property = data[groupProperty];
15
- if (!Object.prototype.hasOwnProperty.call(groupedData, property)) {
16
- groupedData[property] = [];
56
+ const [field, ...rest] = fields;
57
+ const groups = new Map();
58
+ for (const row of dataSet) {
59
+ const key = groupKey(row, field);
60
+ if (!groups.has(key)) {
61
+ groups.set(key, key === 'missing' ? {
62
+ field,
63
+ missing: true,
64
+ rows: []
65
+ } : {
66
+ field,
67
+ value: row[field],
68
+ rows: []
69
+ });
17
70
  }
18
- groupedData[property].push(data);
19
- return groupedData;
20
- }, {});
71
+ groups.get(key).rows.push(row);
72
+ }
73
+ return [...groups.values()].map(({
74
+ rows,
75
+ ...group
76
+ }) => rest.length ? {
77
+ ...group,
78
+ groups: groupBy(rows, rest)
79
+ } : {
80
+ ...group,
81
+ rows
82
+ });
21
83
  };
22
84
  exports.groupBy = groupBy;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.groupBy=void 0,require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.reduce.js");const groupBy=(e=[],r="")=>r?e.reduce(((e,o)=>{const t=o[r];return Object.prototype.hasOwnProperty.call(e,t)||(e[t]=[]),e[t].push(o),e}),{}):e;exports.groupBy=groupBy;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.groupBy=void 0,require("core-js/modules/es.json.stringify.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.map.js"),require("core-js/modules/esnext.iterator.reduce.js"),require("core-js/modules/esnext.map.delete-all.js"),require("core-js/modules/esnext.map.every.js"),require("core-js/modules/esnext.map.filter.js"),require("core-js/modules/esnext.map.find.js"),require("core-js/modules/esnext.map.find-key.js"),require("core-js/modules/esnext.map.includes.js"),require("core-js/modules/esnext.map.key-of.js"),require("core-js/modules/esnext.map.map-keys.js"),require("core-js/modules/esnext.map.map-values.js"),require("core-js/modules/esnext.map.merge.js"),require("core-js/modules/esnext.map.reduce.js"),require("core-js/modules/esnext.map.some.js"),require("core-js/modules/esnext.map.update.js");const sortedKeys=e=>Array.isArray(e)?e.map(sortedKeys):null!==e&&"object"==typeof e?Object.keys(e).sort().reduce(((s,r)=>(s[r]=sortedKeys(e[r]),s)),{}):e,groupKey=(e,s)=>{if(!Object.prototype.hasOwnProperty.call(e,s))return"missing";const r=e[s];return null!==r&&"object"==typeof r?`json:${JSON.stringify(sortedKeys(r))}`:`${typeof r}:${String(r)}`},groupBy=(e=[],s=[])=>{if(!s.length)return e;const[r,...o]=s,t=new Map;for(const s of e){const e=groupKey(s,r);t.has(e)||t.set(e,"missing"===e?{field:r,missing:!0,rows:[]}:{field:r,value:s[r],rows:[]}),t.get(e).rows.push(s)}return[...t.values()].map((({rows:e,...s})=>o.length?{...s,groups:groupBy(e,o)}:{...s,rows:e}))};exports.groupBy=groupBy;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Slice a dataSet down to at most `limit` rows, starting after the first `offset` of them.
2
+ * Slice a dataSet down to at most `limit` rows (or groups, after group by), starting after the first `offset` of them.
3
3
  * @param dataSet
4
4
  * @param offset
5
5
  * @param limit
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.limitOffset = void 0;
7
7
  /**
8
- * Slice a dataSet down to at most `limit` rows, starting after the first `offset` of them.
8
+ * Slice a dataSet down to at most `limit` rows (or groups, after group by), starting after the first `offset` of them.
9
9
  * @param dataSet
10
10
  * @param offset
11
11
  * @param limit
@@ -1,10 +1,12 @@
1
1
  import { ParsedSelect } from '../utilities/parsers/findSelects';
2
+ import { Group } from './groupBy';
2
3
  /**
3
- * Shape each row down to only the selected properties (aliased, when given). dataSet is either a plain array of
4
- * rows, or (once groupBy has run) a plain object keyed by group value, each value an array of rows - both are
5
- * shaped the same way, row by row.
4
+ * Shape each row down to only the selected properties (aliased, when given). dataSet is either a list of rows, or
5
+ * (once groupBy has run) a list of groups, whose rows are shaped the same way at any depth; the groups keep their
6
+ * field and value, selected or not.
6
7
  * @param dataSet
7
8
  * @param selectClauses
9
+ * @param grouped Whether dataSet is a list of groups (see groupBy)
8
10
  */
9
- export declare const selectFields: (dataSet?: Object[] | Object, selectClauses?: ParsedSelect[]) => Object[] | Object;
11
+ export declare const selectFields: (dataSet?: Object[] | Group[], selectClauses?: ParsedSelect[], grouped?: boolean) => Object[] | Group[];
10
12
  //# sourceMappingURL=selectFields.d.ts.map
@@ -14,23 +14,28 @@ const shapeRow = (row, selectClauses) => selectClauses.reduce((shaped, {
14
14
  shaped[alias || property] = row[property];
15
15
  return shaped;
16
16
  }, {});
17
+ const shapeGroup = (group, selectClauses) => group.groups ? {
18
+ ...group,
19
+ groups: group.groups.map(inner => shapeGroup(inner, selectClauses))
20
+ } : {
21
+ ...group,
22
+ rows: group.rows.map(row => shapeRow(row, selectClauses))
23
+ };
17
24
  /**
18
- * Shape each row down to only the selected properties (aliased, when given). dataSet is either a plain array of
19
- * rows, or (once groupBy has run) a plain object keyed by group value, each value an array of rows - both are
20
- * shaped the same way, row by row.
25
+ * Shape each row down to only the selected properties (aliased, when given). dataSet is either a list of rows, or
26
+ * (once groupBy has run) a list of groups, whose rows are shaped the same way at any depth; the groups keep their
27
+ * field and value, selected or not.
21
28
  * @param dataSet
22
29
  * @param selectClauses
30
+ * @param grouped Whether dataSet is a list of groups (see groupBy)
23
31
  */
24
- const selectFields = (dataSet = [], selectClauses = []) => {
32
+ const selectFields = (dataSet = [], selectClauses = [], grouped = false) => {
25
33
  if (!selectClauses.length) {
26
34
  return dataSet;
27
35
  }
28
- if (Array.isArray(dataSet)) {
29
- return dataSet.map(row => shapeRow(row, selectClauses));
36
+ if (grouped) {
37
+ return dataSet.map(group => shapeGroup(group, selectClauses));
30
38
  }
31
- return Object.keys(dataSet).reduce((shaped, key) => {
32
- shaped[key] = dataSet[key].map(row => shapeRow(row, selectClauses));
33
- return shaped;
34
- }, {});
39
+ return dataSet.map(row => shapeRow(row, selectClauses));
35
40
  };
36
41
  exports.selectFields = selectFields;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.selectFields=void 0,require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.map.js"),require("core-js/modules/esnext.iterator.reduce.js");const shapeRow=(e,s)=>s.reduce(((s,{property:r,alias:t})=>(s[t||r]=e[r],s)),{}),selectFields=(e=[],s=[])=>s.length?Array.isArray(e)?e.map((e=>shapeRow(e,s))):Object.keys(e).reduce(((r,t)=>(r[t]=e[t].map((e=>shapeRow(e,s))),r)),{}):e;exports.selectFields=selectFields;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.selectFields=void 0,require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.map.js"),require("core-js/modules/esnext.iterator.reduce.js");const shapeRow=(e,s)=>s.reduce(((s,{property:r,alias:o})=>(s[o||r]=e[r],s)),{}),shapeGroup=(e,s)=>e.groups?{...e,groups:e.groups.map((e=>shapeGroup(e,s)))}:{...e,rows:e.rows.map((e=>shapeRow(e,s)))},selectFields=(e=[],s=[],r=!1)=>s.length?r?e.map((e=>shapeGroup(e,s))):e.map((e=>shapeRow(e,s))):e;exports.selectFields=selectFields;
@@ -2,9 +2,10 @@ import { ParsedQuery } from './parseQuery';
2
2
  export type ParsedCommand = string | 'delete' | 'insert' | 'read' | 'update';
3
3
  export type ParsedEntity = string;
4
4
  /**
5
- * Extract command clauses from the query.
5
+ * Read the command and record name from the start of a query's head (see splitClauses).
6
6
  * @param parsed
7
7
  * @param query
8
+ * @returns The rest of the head (a joined record, `.joined`, for findJoinEntity).
8
9
  */
9
10
  export declare const findCommand: (parsed: ParsedQuery, query: string) => string;
10
11
  //# sourceMappingURL=findCommand.d.ts.map
@@ -4,21 +4,21 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.findCommand = void 0;
7
- var _removeFromQuery = require("./removeFromQuery");
8
- const commandMatch = /^(delete|insert|read|update)\s+([a-z0-9_-]+)[\w\n]*/i;
7
+ const commandMatch = /^(delete|insert|read|update)\s+([a-z0-9_-]+)/i;
9
8
  /**
10
- * Extract command clauses from the query.
9
+ * Read the command and record name from the start of a query's head (see splitClauses).
11
10
  * @param parsed
12
11
  * @param query
12
+ * @returns The rest of the head (a joined record, `.joined`, for findJoinEntity).
13
13
  */
14
14
  const findCommand = (parsed, query) => {
15
15
  const commandFound = query.match(commandMatch);
16
16
  parsed['command'] = undefined;
17
17
  parsed['entity'] = undefined;
18
18
  if (commandFound) {
19
- parsed['command'] = commandFound[1];
19
+ parsed['command'] = commandFound[1].toLowerCase();
20
20
  parsed['entity'] = commandFound[2];
21
- return (0, _removeFromQuery.removeFromQuery)(commandFound[0], query);
21
+ return query.slice(commandFound[0].length).trim();
22
22
  }
23
23
  return query;
24
24
  };
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.findCommand=void 0;var _removeFromQuery=require("./removeFromQuery");const commandMatch=/^(delete|insert|read|update)\s+([a-z0-9_-]+)[\w\n]*/i,findCommand=(e,o)=>{const m=o.match(commandMatch);return e.command=void 0,e.entity=void 0,m?(e.command=m[1],e.entity=m[2],(0,_removeFromQuery.removeFromQuery)(m[0],o)):o};exports.findCommand=findCommand;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.findCommand=void 0;const commandMatch=/^(delete|insert|read|update)\s+([a-z0-9_-]+)/i,findCommand=(e,t)=>{const d=t.match(commandMatch);return e.command=void 0,e.entity=void 0,d?(e.command=d[1].toLowerCase(),e.entity=d[2],t.slice(d[0].length).trim()):t};exports.findCommand=findCommand;
@@ -8,15 +8,14 @@ export type ParsedCondition = {
8
8
  type?: string;
9
9
  };
10
10
  /**
11
- * Extract the where clause from the query into a tree (see ConditionNode): `where` followed by conditions joined with
12
- * `and` / `or`, `not`, and brackets, nested to any depth. Each condition is `<field> <comparator> <value>`, or
13
- * `<field> is null`, `is not null`, `exists` or `does not exist`. A comparison with the literal null (`= null`) is
14
- * refused: it would never match, as null equals nothing (see where). The expression ends at the next clause keyword
15
- * outside brackets, so clauses may come in any order; several separate `where`s (or `and where` after another clause)
16
- * are joined with `and`, as before.
11
+ * Read a where clause body (see splitClauses) into a tree (see ConditionNode): conditions joined with `and` / `or`,
12
+ * `not`, and brackets, nested to any depth. Each condition is `<field> <comparator> <value>`, or `<field> is null`,
13
+ * `is not null`, `exists` or `does not exist`. A comparison with the literal null (`= null`) is refused: it would never
14
+ * match, as null equals nothing (see where).
17
15
  * @param parsed
18
- * @param query
19
- * @throws {Error} For the errors readExpression and readCondition describe, and `or where` after another clause.
16
+ * @param body
17
+ * @returns The rest of the body, after the expression.
18
+ * @throws {Error} For the errors readExpression and readCondition describe.
20
19
  */
21
- export declare const findConditions: (parsed: ParsedQuery, query: string) => string;
20
+ export declare const findConditions: (parsed: ParsedQuery, body: string) => string;
22
21
  //# sourceMappingURL=findConditions.d.ts.map
@@ -10,15 +10,13 @@ require("core-js/modules/esnext.iterator.flat-map.js");
10
10
  var _parseLiteralValue = require("./parseLiteralValue");
11
11
  var _conditionTree = require("../conditionTree");
12
12
  var _maskStrings = require("./maskStrings");
13
+ var _splitClauses = require("./splitClauses");
13
14
  // Keywords count only as whole words: in `on t.x = u.color where ...` the `or` is part of the name `color`
14
15
  const wordEnd = '(?![a-z0-9_.-])';
15
16
  const keyword = words => new RegExp(`^(?:${words})${wordEnd}`, 'i');
16
17
  // One condition, at the start of the text: a field, then `is [not] null` / `exists` / `does not exist`, or a comparator
17
18
  // and a value (strings and arrays are placeholders by then, see maskStrings)
18
19
  const conditionMatch = new RegExp(`^([a-z0-9_-]+\\.?[a-z0-9_-]*)\\s+(?:(is\\s+not\\s+null|is\\s+null|exists|does\\s+not\\s+exist)${wordEnd}|(<>|!=|>=|<=|=|>|<|in|between|like)\\s+(${_maskStrings.placeholderSource}|(${_parseLiteralValue.numberSource})|${_maskStrings.arraySource}|(null)|(true)|(false)))`, 'i');
19
- // The clause keywords that end a where expression (outside brackets), and `and on` (a join clause's own `and`)
20
- const expressionEnd = keyword('sort|group|limit|offset|select|merge|on|where|and\\s+on');
21
- const whereMatch = new RegExp(`(^|[^a-z0-9._-])((?:(and|or)\\s+)?where)${wordEnd}`, 'i');
22
20
  /**
23
21
  * Read one condition from a match of conditionMatch.
24
22
  * @param match
@@ -62,15 +60,15 @@ const join = (kind, nodes) => {
62
60
  };
63
61
  };
64
62
  /**
65
- * Read one where expression from the masked query, starting at `start` (just after its `where`). Precedence, loosest
66
- * first: `or`, `and`, `not`; brackets group. `and where` / `or where` are the older spellings of `and` / `or`.
63
+ * Read one where expression from the start of a where clause body (masked, see maskStrings). Precedence, loosest
64
+ * first: `or`, `and`, `not`; brackets group.
67
65
  * @param text
68
- * @param start
69
- * @returns The expression's tree and where it ends (the next clause keyword outside brackets, or the end).
70
- * @throws {Error} For an unbalanced bracket, empty brackets, or an operator with no condition after it.
66
+ * @returns The expression's tree and where it ends (at the first text that can't carry it on, or the end).
67
+ * @throws {Error} For an unbalanced bracket, empty brackets, an operator with no condition after it, or two conditions
68
+ * with no and/or between them.
71
69
  */
72
- const readExpression = (text, start) => {
73
- let at = start;
70
+ const readExpression = text => {
71
+ let at = 0;
74
72
  const rest = () => text.slice(at);
75
73
  const skipSpace = () => {
76
74
  at += rest().length - rest().trimStart().length;
@@ -93,6 +91,10 @@ const readExpression = (text, start) => {
93
91
  at += condition[0].length;
94
92
  return readCondition(condition);
95
93
  }
94
+ // The 2.x `and where` / `or where` inside brackets, where splitClauses doesn't look
95
+ if ((after === 'and' || after === 'or') && keyword('where').test(rest())) {
96
+ throw new Error(`"${after} where" is no longer supported: ${_splitClauses.whereHint}`);
97
+ }
96
98
  if (take(keyword('not'))) {
97
99
  return {
98
100
  not: factor('not')
@@ -115,11 +117,7 @@ const readExpression = (text, start) => {
115
117
  const term = after => {
116
118
  const nodes = [factor(after)];
117
119
  for (;;) {
118
- skipSpace();
119
- if (keyword('and\\s+on').test(rest())) {
120
- break;
121
- }
122
- const connector = take(keyword('and(?:\\s+where)?'));
120
+ const connector = take(keyword('and'));
123
121
  if (!connector) {
124
122
  break;
125
123
  }
@@ -130,7 +128,7 @@ const readExpression = (text, start) => {
130
128
  const expression = after => {
131
129
  const nodes = [term(after)];
132
130
  for (;;) {
133
- const connector = take(keyword('or(?:\\s+where)?'));
131
+ const connector = take(keyword('or'));
134
132
  if (!connector) {
135
133
  break;
136
134
  }
@@ -143,7 +141,7 @@ const readExpression = (text, start) => {
143
141
  if (rest().startsWith(')')) {
144
142
  throw new Error(`Unexpected ) in a where clause: ${shown()}`);
145
143
  }
146
- if (rest() && !expressionEnd.test(rest()) && conditionMatch.test(rest())) {
144
+ if (conditionMatch.test(rest())) {
147
145
  throw new Error(`Missing and/or before: ${shown()}`);
148
146
  }
149
147
  return {
@@ -152,31 +150,21 @@ const readExpression = (text, start) => {
152
150
  };
153
151
  };
154
152
  /**
155
- * Extract the where clause from the query into a tree (see ConditionNode): `where` followed by conditions joined with
156
- * `and` / `or`, `not`, and brackets, nested to any depth. Each condition is `<field> <comparator> <value>`, or
157
- * `<field> is null`, `is not null`, `exists` or `does not exist`. A comparison with the literal null (`= null`) is
158
- * refused: it would never match, as null equals nothing (see where). The expression ends at the next clause keyword
159
- * outside brackets, so clauses may come in any order; several separate `where`s (or `and where` after another clause)
160
- * are joined with `and`, as before.
153
+ * Read a where clause body (see splitClauses) into a tree (see ConditionNode): conditions joined with `and` / `or`,
154
+ * `not`, and brackets, nested to any depth. Each condition is `<field> <comparator> <value>`, or `<field> is null`,
155
+ * `is not null`, `exists` or `does not exist`. A comparison with the literal null (`= null`) is refused: it would never
156
+ * match, as null equals nothing (see where).
161
157
  * @param parsed
162
- * @param query
163
- * @throws {Error} For the errors readExpression and readCondition describe, and `or where` after another clause.
158
+ * @param body
159
+ * @returns The rest of the body, after the expression.
160
+ * @throws {Error} For the errors readExpression and readCondition describe.
164
161
  */
165
- const findConditions = (parsed, query) => {
166
- const expressions = [];
167
- for (let found = query.match(whereMatch); found; found = query.match(whereMatch)) {
168
- if ((found[3] || '').toLowerCase() === 'or') {
169
- throw new Error('`or where` must follow the conditions it joins: put them in one where clause');
170
- }
171
- const whereStart = found.index + found[1].length;
172
- const {
173
- node,
174
- end
175
- } = readExpression(query, whereStart + found[2].length);
176
- expressions.push(node);
177
- query = `${query.slice(0, whereStart)} ${query.slice(end)}`.replace(/\s+/g, ' ').trim();
178
- }
179
- parsed['where'] = expressions.length ? join('and', expressions) : (0, _conditionTree.allOf)([]);
180
- return query;
162
+ const findConditions = (parsed, body) => {
163
+ const {
164
+ node,
165
+ end
166
+ } = readExpression(body);
167
+ parsed['where'] = node;
168
+ return body.slice(end).trim();
181
169
  };
182
170
  exports.findConditions = findConditions;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.findConditions=void 0,require("core-js/modules/es.array.includes.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.flat-map.js");var _parseLiteralValue=require("./parseLiteralValue"),_conditionTree=require("../conditionTree"),_maskStrings=require("./maskStrings");const wordEnd="(?![a-z0-9_.-])",keyword=e=>new RegExp(`^(?:${e})${wordEnd}`,"i"),conditionMatch=new RegExp(`^([a-z0-9_-]+\\.?[a-z0-9_-]*)\\s+(?:(is\\s+not\\s+null|is\\s+null|exists|does\\s+not\\s+exist)${wordEnd}|(<>|!=|>=|<=|=|>|<|in|between|like)\\s+(${_maskStrings.placeholderSource}|(${_parseLiteralValue.numberSource})|${_maskStrings.arraySource}|(null)|(true)|(false)))`,"i"),expressionEnd=keyword("sort|group|limit|offset|select|merge|on|where|and\\s+on"),whereMatch=new RegExp(`(^|[^a-z0-9._-])((?:(and|or)\\s+)?where)${wordEnd}`,"i"),readCondition=e=>{const r=e[1];if(void 0!==e[2])return{property:r,comparator:e[2].toLowerCase().replace(/\s+/g," "),value:null};const n=e[3].toLowerCase(),t=(0,_parseLiteralValue.parseLiteralValue)(e,5);if(null===t)throw new Error(`\`${r} ${n} null\` never matches (null equals nothing, as in SQL): use \`${r} is null\` or \`${r} is not null\``);if("between"===n&&Array.isArray(t)&&t.includes(null))throw new Error(`\`${r} between\` can't have a null bound: it would never match`);return{property:r,comparator:n,value:t}},join=(e,r)=>{const n=r.flatMap((r=>!(0,_conditionTree.isCondition)(r)&&e in r?r[e]:[r]));return 1===n.length?n[0]:"and"===e?{and:n}:{or:n}},readExpression=(e,r)=>{let n=r;const t=()=>e.slice(n),o=()=>{n+=t().length-t().trimStart().length},s=e=>{o();const r=t().match(e);if(r)return n+=r[0].length,r[0]},i=()=>(0,_maskStrings.unmaskStrings)(t().trim(),"'"),a=e=>{o();const r=t().match(conditionMatch);if(r)return n+=r[0].length,readCondition(r);if(s(keyword("not")))return{not:a("not")};if(s(/^\(/)){if(o(),t().startsWith(")"))throw new Error("Empty brackets in a where clause");const e=d("(");if(!s(/^\)/))throw new Error("Missing ) "+(t().trim()?`before: ${i()}`:"at the end of the where clause"));return e}throw new Error(`Missing a condition after ${e}${t().trim()?`: ${i()}`:""}`)},l=e=>{const r=[a(e)];for(;o(),!keyword("and\\s+on").test(t());){const e=s(keyword("and(?:\\s+where)?"));if(!e)break;r.push(a(e.toLowerCase().replace(/\s+/g," ")))}return join("and",r)},d=e=>{const r=[l(e)];for(;;){const e=s(keyword("or(?:\\s+where)?"));if(!e)break;r.push(l(e.toLowerCase().replace(/\s+/g," ")))}return join("or",r)},c=d("where");if(o(),t().startsWith(")"))throw new Error(`Unexpected ) in a where clause: ${i()}`);if(t()&&!expressionEnd.test(t())&&conditionMatch.test(t()))throw new Error(`Missing and/or before: ${i()}`);return{node:c,end:n}},findConditions=(e,r)=>{const n=[];for(let e=r.match(whereMatch);e;e=r.match(whereMatch)){if("or"===(e[3]||"").toLowerCase())throw new Error("`or where` must follow the conditions it joins: put them in one where clause");const t=e.index+e[1].length,{node:o,end:s}=readExpression(r,t+e[2].length);n.push(o),r=`${r.slice(0,t)} ${r.slice(s)}`.replace(/\s+/g," ").trim()}return e.where=n.length?join("and",n):(0,_conditionTree.allOf)([]),r};exports.findConditions=findConditions;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.findConditions=void 0,require("core-js/modules/es.array.includes.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.flat-map.js");var _parseLiteralValue=require("./parseLiteralValue"),_conditionTree=require("../conditionTree"),_maskStrings=require("./maskStrings"),_splitClauses=require("./splitClauses");const wordEnd="(?![a-z0-9_.-])",keyword=e=>new RegExp(`^(?:${e})${wordEnd}`,"i"),conditionMatch=new RegExp(`^([a-z0-9_-]+\\.?[a-z0-9_-]*)\\s+(?:(is\\s+not\\s+null|is\\s+null|exists|does\\s+not\\s+exist)${wordEnd}|(<>|!=|>=|<=|=|>|<|in|between|like)\\s+(${_maskStrings.placeholderSource}|(${_parseLiteralValue.numberSource})|${_maskStrings.arraySource}|(null)|(true)|(false)))`,"i"),readCondition=e=>{const r=e[1];if(void 0!==e[2])return{property:r,comparator:e[2].toLowerCase().replace(/\s+/g," "),value:null};const n=e[3].toLowerCase(),t=(0,_parseLiteralValue.parseLiteralValue)(e,5);if(null===t)throw new Error(`\`${r} ${n} null\` never matches (null equals nothing, as in SQL): use \`${r} is null\` or \`${r} is not null\``);if("between"===n&&Array.isArray(t)&&t.includes(null))throw new Error(`\`${r} between\` can't have a null bound: it would never match`);return{property:r,comparator:n,value:t}},join=(e,r)=>{const n=r.flatMap((r=>!(0,_conditionTree.isCondition)(r)&&e in r?r[e]:[r]));return 1===n.length?n[0]:"and"===e?{and:n}:{or:n}},readExpression=e=>{let r=0;const n=()=>e.slice(r),t=()=>{r+=n().length-n().trimStart().length},o=e=>{t();const o=n().match(e);if(o)return r+=o[0].length,o[0]},s=()=>(0,_maskStrings.unmaskStrings)(n().trim(),"'"),i=e=>{t();const a=n().match(conditionMatch);if(a)return r+=a[0].length,readCondition(a);if(("and"===e||"or"===e)&&keyword("where").test(n()))throw new Error(`"${e} where" is no longer supported: ${_splitClauses.whereHint}`);if(o(keyword("not")))return{not:i("not")};if(o(/^\(/)){if(t(),n().startsWith(")"))throw new Error("Empty brackets in a where clause");const e=l("(");if(!o(/^\)/))throw new Error("Missing ) "+(n().trim()?`before: ${s()}`:"at the end of the where clause"));return e}throw new Error(`Missing a condition after ${e}${n().trim()?`: ${s()}`:""}`)},a=e=>{const r=[i(e)];for(;;){const e=o(keyword("and"));if(!e)break;r.push(i(e.toLowerCase().replace(/\s+/g," ")))}return join("and",r)},l=e=>{const r=[a(e)];for(;;){const e=o(keyword("or"));if(!e)break;r.push(a(e.toLowerCase().replace(/\s+/g," ")))}return join("or",r)},u=l("where");if(t(),n().startsWith(")"))throw new Error(`Unexpected ) in a where clause: ${s()}`);if(conditionMatch.test(n()))throw new Error(`Missing and/or before: ${s()}`);return{node:u,end:r}},findConditions=(e,r)=>{const{node:n,end:t}=readExpression(r);return e.where=n,r.slice(t).trim()};exports.findConditions=findConditions;
@@ -1,9 +1,11 @@
1
1
  import { ParsedQuery } from './parseQuery';
2
- export type ParsedGroupBy = string;
2
+ export type ParsedGroupBy = string[];
3
3
  /**
4
- * Extract the group by clauses from the query.
4
+ * Read a group by clause body (`a, b`, see splitClauses).
5
5
  * @param parsed
6
- * @param query
6
+ * @param body
7
+ * @returns The rest of the body, after the last field.
8
+ * @throws {Error} For a field named twice.
7
9
  */
8
- export declare const findGroupBy: (parsed: ParsedQuery, query: string) => string;
10
+ export declare const findGroupBy: (parsed: ParsedQuery, body: string) => string;
9
11
  //# sourceMappingURL=findGroupBy.d.ts.map
@@ -4,19 +4,27 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.findGroupBy = void 0;
7
- var _removeFromQuery = require("./removeFromQuery");
8
- const groupMatch = /group\s+by\s+(,?[a-z0-9._-]+)+/i;
7
+ require("core-js/modules/esnext.iterator.constructor.js");
8
+ require("core-js/modules/esnext.iterator.find.js");
9
+ const groupMatch = /^[a-z0-9._-]+(?:\s*,\s*[a-z0-9._-]+)*/i;
9
10
  /**
10
- * Extract the group by clauses from the query.
11
+ * Read a group by clause body (`a, b`, see splitClauses).
11
12
  * @param parsed
12
- * @param query
13
+ * @param body
14
+ * @returns The rest of the body, after the last field.
15
+ * @throws {Error} For a field named twice.
13
16
  */
14
- const findGroupBy = (parsed, query) => {
15
- const groupFound = query.match(groupMatch);
17
+ const findGroupBy = (parsed, body) => {
18
+ const groupFound = body.match(groupMatch);
16
19
  if (groupFound) {
17
- parsed['groupBy'] = groupFound ? groupFound[1] : undefined;
18
- return (0, _removeFromQuery.removeFromQuery)(groupFound[0], query);
20
+ const fields = groupFound[0].split(/\s*,\s*/);
21
+ const repeated = fields.find((field, index) => fields.indexOf(field) !== index);
22
+ if (repeated) {
23
+ throw new Error(`"${repeated}" is in group by twice`);
24
+ }
25
+ parsed['groupBy'] = fields;
26
+ return body.slice(groupFound[0].length).trim();
19
27
  }
20
- return query;
28
+ return body;
21
29
  };
22
30
  exports.findGroupBy = findGroupBy;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.findGroupBy=void 0;var _removeFromQuery=require("./removeFromQuery");const groupMatch=/group\s+by\s+(,?[a-z0-9._-]+)+/i,findGroupBy=(r,e)=>{const o=e.match(groupMatch);return o?(r.groupBy=o?o[1]:void 0,(0,_removeFromQuery.removeFromQuery)(o[0],e)):e};exports.findGroupBy=findGroupBy;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.findGroupBy=void 0,require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.find.js");const groupMatch=/^[a-z0-9._-]+(?:\s*,\s*[a-z0-9._-]+)*/i,findGroupBy=(r,e)=>{const t=e.match(groupMatch);if(t){const o=t[0].split(/\s*,\s*/),s=o.find(((r,e)=>o.indexOf(r)!==e));if(s)throw new Error(`"${s}" is in group by twice`);return r.groupBy=o,e.slice(t[0].length).trim()}return e};exports.findGroupBy=findGroupBy;
@@ -3,11 +3,12 @@ export type ParsedInsertRow = {
3
3
  [field: string]: any;
4
4
  };
5
5
  /**
6
- * Extract an insert's "values field = value, field2 = value2" clause from the query, the same way
6
+ * Read an insert's values clause body (`field = value, field2 = value2`, see splitClauses), the same way
7
7
  * findUpdateValues() does for "set". insertEntity() accepts an array of rows for bulk inserts, but
8
8
  * a query string only describes a single row for now - wrapped in a one-element array to match.
9
9
  * @param parsed
10
- * @param query
10
+ * @param body
11
+ * @returns The rest of the body, after the last assignment.
11
12
  */
12
- export declare const findInsertValues: (parsed: ParsedQuery, query: string) => string;
13
+ export declare const findInsertValues: (parsed: ParsedQuery, body: string) => string;
13
14
  //# sourceMappingURL=findInsertValues.d.ts.map
@@ -4,32 +4,32 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.findInsertValues = void 0;
7
- var _removeFromQuery = require("./removeFromQuery");
8
7
  var _parseLiteralValue = require("./parseLiteralValue");
9
8
  var _maskStrings = require("./maskStrings");
10
9
  const literal = `(?:${_maskStrings.placeholderSource.replace('(', '(?:')}|${_parseLiteralValue.numberSource}|${_maskStrings.arraySource.replace('(', '(?:')}|null|true|false)`;
11
- const valuesClauseMatch = new RegExp(`(values)\\s+([a-z0-9_-]+\\s*=\\s*${literal}(?:\\s*,\\s*[a-z0-9_-]+\\s*=\\s*${literal})*)`, 'i');
10
+ const valuesMatch = new RegExp(`^([a-z0-9_-]+\\s*=\\s*${literal}(?:\\s*,\\s*[a-z0-9_-]+\\s*=\\s*${literal})*)`, 'i');
12
11
  const assignmentMatch = new RegExp(`([a-z0-9_-]+)\\s*=\\s*(${_maskStrings.placeholderSource}|(${_parseLiteralValue.numberSource})|${_maskStrings.arraySource}|(null)|(true)|(false))`, 'ig');
13
12
  /**
14
- * Extract an insert's "values field = value, field2 = value2" clause from the query, the same way
13
+ * Read an insert's values clause body (`field = value, field2 = value2`, see splitClauses), the same way
15
14
  * findUpdateValues() does for "set". insertEntity() accepts an array of rows for bulk inserts, but
16
15
  * a query string only describes a single row for now - wrapped in a one-element array to match.
17
16
  * @param parsed
18
- * @param query
17
+ * @param body
18
+ * @returns The rest of the body, after the last assignment.
19
19
  */
20
- const findInsertValues = (parsed, query) => {
21
- const clauseFound = query.match(valuesClauseMatch);
20
+ const findInsertValues = (parsed, body) => {
21
+ const clauseFound = body.match(valuesMatch);
22
22
  parsed['insertValues'] = [];
23
23
  if (!clauseFound) {
24
- return query;
24
+ return body;
25
25
  }
26
26
  const row = {};
27
- const assignmentsFound = clauseFound[2].matchAll(assignmentMatch);
27
+ const assignmentsFound = clauseFound[1].matchAll(assignmentMatch);
28
28
  // @ts-ignore
29
29
  for (const assignment of assignmentsFound) {
30
30
  row[assignment[1]] = (0, _parseLiteralValue.parseLiteralValue)(assignment, 3);
31
31
  }
32
32
  parsed['insertValues'] = [row];
33
- return (0, _removeFromQuery.removeFromQuery)(clauseFound[0], query);
33
+ return body.slice(clauseFound[0].length).trim();
34
34
  };
35
35
  exports.findInsertValues = findInsertValues;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.findInsertValues=void 0;var _removeFromQuery=require("./removeFromQuery"),_parseLiteralValue=require("./parseLiteralValue"),_maskStrings=require("./maskStrings");const literal=`(?:${_maskStrings.placeholderSource.replace("(","(?:")}|${_parseLiteralValue.numberSource}|${_maskStrings.arraySource.replace("(","(?:")}|null|true|false)`,valuesClauseMatch=new RegExp(`(values)\\s+([a-z0-9_-]+\\s*=\\s*${literal}(?:\\s*,\\s*[a-z0-9_-]+\\s*=\\s*${literal})*)`,"i"),assignmentMatch=new RegExp(`([a-z0-9_-]+)\\s*=\\s*(${_maskStrings.placeholderSource}|(${_parseLiteralValue.numberSource})|${_maskStrings.arraySource}|(null)|(true)|(false))`,"ig"),findInsertValues=(e,r)=>{const a=r.match(valuesClauseMatch);if(e.insertValues=[],!a)return r;const s={},l=a[2].matchAll(assignmentMatch);for(const e of l)s[e[1]]=(0,_parseLiteralValue.parseLiteralValue)(e,3);return e.insertValues=[s],(0,_removeFromQuery.removeFromQuery)(a[0],r)};exports.findInsertValues=findInsertValues;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.findInsertValues=void 0;var _parseLiteralValue=require("./parseLiteralValue"),_maskStrings=require("./maskStrings");const literal=`(?:${_maskStrings.placeholderSource.replace("(","(?:")}|${_parseLiteralValue.numberSource}|${_maskStrings.arraySource.replace("(","(?:")}|null|true|false)`,valuesMatch=new RegExp(`^([a-z0-9_-]+\\s*=\\s*${literal}(?:\\s*,\\s*[a-z0-9_-]+\\s*=\\s*${literal})*)`,"i"),assignmentMatch=new RegExp(`([a-z0-9_-]+)\\s*=\\s*(${_maskStrings.placeholderSource}|(${_parseLiteralValue.numberSource})|${_maskStrings.arraySource}|(null)|(true)|(false))`,"ig"),findInsertValues=(e,r)=>{const a=r.match(valuesMatch);if(e.insertValues=[],!a)return r;const s={},t=a[1].matchAll(assignmentMatch);for(const e of t)s[e[1]]=(0,_parseLiteralValue.parseLiteralValue)(e,3);return e.insertValues=[s],r.slice(a[0].length).trim()};exports.findInsertValues=findInsertValues;
@@ -5,9 +5,10 @@ export type ParsedJoinClause = {
5
5
  propertyB: string;
6
6
  };
7
7
  /**
8
- * Extract condition clauses from the query.
8
+ * Read an on clause body (`a.x = b.y and on b.z = c.w`, see splitClauses).
9
9
  * @param parsed
10
- * @param query
10
+ * @param body
11
+ * @returns The rest of the body, after the last join condition.
11
12
  */
12
- export declare const findJoinClauses: (parsed: ParsedQuery, query: string) => string;
13
+ export declare const findJoinClauses: (parsed: ParsedQuery, body: string) => string;
13
14
  //# sourceMappingURL=findJoinClauses.d.ts.map
@@ -4,24 +4,31 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.findJoinClauses = void 0;
7
- var _removeFromQuery = require("./removeFromQuery");
8
- const joinClauseMatch = /(and)?\s*on\s+([a-z0-9_-]+\.[a-z0-9_-]+)\s+(=|!=|>|<|>=|<=|in|between|like)\s+([a-z0-9_-]+\.[a-z0-9_-]+)/ig;
7
+ // One join condition, at the start of the text; every one after the first starts with `and on` (see splitClauses)
8
+ const joinClauseMatch = /^([a-z0-9_-]+\.[a-z0-9_-]+)\s+(=|!=|>|<|>=|<=|in|between|like)\s+([a-z0-9_-]+\.[a-z0-9_-]+)(?![a-z0-9._-])/i;
9
+ const nextMatch = /^and\s+on\s+/i;
9
10
  /**
10
- * Extract condition clauses from the query.
11
+ * Read an on clause body (`a.x = b.y and on b.z = c.w`, see splitClauses).
11
12
  * @param parsed
12
- * @param query
13
+ * @param body
14
+ * @returns The rest of the body, after the last join condition.
13
15
  */
14
- const findJoinClauses = (parsed, query) => {
15
- const joinClausesFound = query.matchAll(joinClauseMatch);
16
- // @ts-ignore
17
- for (const joinClause of joinClausesFound) {
16
+ const findJoinClauses = (parsed, body) => {
17
+ let rest = body;
18
+ for (let joinClause = rest.match(joinClauseMatch); joinClause; joinClause = rest.match(joinClauseMatch)) {
18
19
  parsed['joinClauses'].push({
19
- propertyA: joinClause[2],
20
- comparator: joinClause[3],
21
- propertyB: joinClause[4]
20
+ propertyA: joinClause[1],
21
+ comparator: joinClause[2],
22
+ propertyB: joinClause[3]
22
23
  });
23
- query = (0, _removeFromQuery.removeFromQuery)(joinClause[0], query);
24
+ rest = rest.slice(joinClause[0].length).trim();
25
+ // The connector only with another one after it, so a bad one is shown with its connector
26
+ const next = rest.match(nextMatch);
27
+ if (!next || !joinClauseMatch.test(rest.slice(next[0].length))) {
28
+ break;
29
+ }
30
+ rest = rest.slice(next[0].length);
24
31
  }
25
- return query;
32
+ return rest;
26
33
  };
27
34
  exports.findJoinClauses = findJoinClauses;