json-fs-query 2.0.0 → 2.1.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.
@@ -41,6 +41,10 @@ const typeCheck = (type = 'string', value = undefined, nullable = false) => {
41
41
  if (Number.isNaN(coerced) || !Number.isInteger(coerced)) {
42
42
  throw new Error(`Value "${value}" is not a valid int`);
43
43
  }
44
+ // Past Number.MAX_SAFE_INTEGER an int can't be held exactly: '9007199254740993' would be stored as ...992
45
+ if (!Number.isSafeInteger(coerced)) {
46
+ throw new Error(`Value "${value}" is too large for an int (the largest is ${Number.MAX_SAFE_INTEGER})`);
47
+ }
44
48
  return coerced;
45
49
  }
46
50
  case 'boolean':
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.typeCheck=exports.fieldTypes=void 0;const fieldTypes=exports.fieldTypes=["string","int","boolean","object","array"],typeCheck=(e="string",r=void 0,t=!1)=>{if(void 0===r)return r;if(null===r){if(t)return r;throw new Error('Value "null" is not allowed: the field is not nullable')}switch(e){case"string":return"string"==typeof r?r:String(r);case"int":{const e=Number(r);if(Number.isNaN(e)||!Number.isInteger(e))throw new Error(`Value "${r}" is not a valid int`);return e}case"boolean":if("boolean"==typeof r)return r;if("true"===r)return!0;if("false"===r)return!1;throw new Error(`Value "${r}" is not a valid boolean`);case"object":if("object"!=typeof r||null===r||Array.isArray(r))throw new Error(`Value "${r}" is not a valid object`);return r;case"array":if(!Array.isArray(r))throw new Error(`Value "${r}" is not a valid array`);return r;default:return r}};exports.typeCheck=typeCheck;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.typeCheck=exports.fieldTypes=void 0;const fieldTypes=exports.fieldTypes=["string","int","boolean","object","array"],typeCheck=(e="string",r=void 0,t=!1)=>{if(void 0===r)return r;if(null===r){if(t)return r;throw new Error('Value "null" is not allowed: the field is not nullable')}switch(e){case"string":return"string"==typeof r?r:String(r);case"int":{const e=Number(r);if(Number.isNaN(e)||!Number.isInteger(e))throw new Error(`Value "${r}" is not a valid int`);if(!Number.isSafeInteger(e))throw new Error(`Value "${r}" is too large for an int (the largest is ${Number.MAX_SAFE_INTEGER})`);return e}case"boolean":if("boolean"==typeof r)return r;if("true"===r)return!0;if("false"===r)return!1;throw new Error(`Value "${r}" is not a valid boolean`);case"object":if("object"!=typeof r||null===r||Array.isArray(r))throw new Error(`Value "${r}" is not a valid object`);return r;case"array":if(!Array.isArray(r))throw new Error(`Value "${r}" is not a valid array`);return r;default:return r}};exports.typeCheck=typeCheck;
@@ -4,6 +4,14 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.sortBy = void 0;
7
+ // Where a value sorts before any other, ascending: missing first, then null, then the values (MySQL's null placement,
8
+ // with missing below null). Descending reverses it.
9
+ const presence = (data, property) => {
10
+ if (!Object.prototype.hasOwnProperty.call(data, property)) {
11
+ return 0;
12
+ }
13
+ return data[property] === null ? 1 : 2;
14
+ };
7
15
  const sortBy = (dataSet = [], sortClauses = []) => {
8
16
  if (!sortClauses.length) {
9
17
  return dataSet;
@@ -12,8 +20,13 @@ const sortBy = (dataSet = [], sortClauses = []) => {
12
20
  for (const sortClause of sortClauses) {
13
21
  const property = sortClause.property;
14
22
  const direction = sortClause.direction;
23
+ const rankA = presence(a, property);
24
+ const rankB = presence(b, property);
25
+ if (rankA !== rankB) {
26
+ return direction === 'asc' ? rankA - rankB : rankB - rankA;
27
+ }
15
28
  // Ordering compares the raw stored values, not by declared type (that's only for equality, see where), so
16
- // numeric ids in a string field still sort numerically. Loose so '3' and 3 still tie.
29
+ // numeric ids in a string field still sort numerically. Loose so '3' and 3 still tie, as do two nulls or two missing.
17
30
  // eslint-disable-next-line eqeqeq
18
31
  if (a[property] == b[property]) {
19
32
  continue;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.sortBy=void 0;const sortBy=(t=[],o=[])=>o.length?(t.sort(((t,r)=>{for(const e of o){const o=e.property,s=e.direction;if(t[o]==r[o])continue;const n=t[o]<r[o]?-1:1;return"asc"===s?n:-1*n}return 0})),t):t;exports.sortBy=sortBy;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.sortBy=void 0;const presence=(e,r)=>Object.prototype.hasOwnProperty.call(e,r)?null===e[r]?1:2:0,sortBy=(e=[],r=[])=>r.length?(e.sort(((e,t)=>{for(const o of r){const r=o.property,s=o.direction,n=presence(e,r),c=presence(t,r);if(n!==c)return"asc"===s?n-c:c-n;if(e[r]==t[r])continue;const p=e[r]<t[r]?-1:1;return"asc"===s?p:-1*p}return 0})),e):e;exports.sortBy=sortBy;
@@ -19,6 +19,12 @@ const likeMatch = (dataValue, pattern) => {
19
19
  /**
20
20
  * Equality (=, !=, <>, in) compares by the field's declared type when given one (see sameValue), so
21
21
  * '3' and 3 match in a string or int field. Ordering (>, <, between, like) compares the raw values.
22
+ *
23
+ * Null and missing are their own cases, as in SQL: a field that is null or missing matches no comparison (not even
24
+ * `!=`), and a null never equals anything, not even another null. Only these test for them:
25
+ * - `is null`: the field is there and holds null
26
+ * - `is not null`: the field is there and holds a value (so a missing field doesn't match either)
27
+ * - `exists` / `does not exist`: the field is there (null counts) / is missing
22
28
  * @param data
23
29
  * @param property
24
30
  * @param comparator
@@ -26,7 +32,18 @@ const likeMatch = (dataValue, pattern) => {
26
32
  * @param type
27
33
  */
28
34
  const makeCondition = (data, property, comparator, value, type = undefined) => {
29
- if (!Object.prototype.hasOwnProperty.call(data, property)) {
35
+ const exists = Object.prototype.hasOwnProperty.call(data, property);
36
+ switch (comparator) {
37
+ case 'exists':
38
+ return exists;
39
+ case 'does not exist':
40
+ return !exists;
41
+ case 'is null':
42
+ return exists && data[property] === null;
43
+ case 'is not null':
44
+ return exists && data[property] !== null;
45
+ }
46
+ if (!exists || data[property] === null || value === null) {
30
47
  return false;
31
48
  }
32
49
  const dataValue = data[property];
@@ -47,7 +64,8 @@ const makeCondition = (data, property, comparator, value, type = undefined) => {
47
64
  case 'in':
48
65
  return Array.isArray(value) && (type ? value.some(item => (0, _compareAs.sameValue)(type, dataValue, item)) : value.includes(dataValue));
49
66
  case 'between':
50
- return Array.isArray(value) && value.length === 2 && dataValue >= value[0] && dataValue <= value[1];
67
+ // A null bound would act as 0 in >= / <=: it bounds nothing, so nothing matches (the parser refuses it)
68
+ return Array.isArray(value) && value.length === 2 && !value.includes(null) && dataValue >= value[0] && dataValue <= value[1];
51
69
  case 'like':
52
70
  return likeMatch(dataValue, value);
53
71
  }
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.where=void 0,require("core-js/modules/es.array.includes.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.filter.js"),require("core-js/modules/esnext.iterator.some.js");var _compareAs=require("../utilities/compareAs");const likeMatch=(e,r)=>{if("string"!=typeof e||"string"!=typeof r)return!1;const t=r.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/%/g,".*").replace(/_/g,".");return new RegExp(`^${t}$`,"i").test(e)},makeCondition=(e,r,t,s,a=void 0)=>{if(!Object.prototype.hasOwnProperty.call(e,r))return!1;const o=e[r];switch(t){case"=":return(0,_compareAs.sameValue)(a,o,s);case"!=":case"<>":return!(0,_compareAs.sameValue)(a,o,s);case">":return o>s;case">=":return o>=s;case"<":return o<s;case"<=":return o<=s;case"in":return Array.isArray(s)&&(a?s.some((e=>(0,_compareAs.sameValue)(a,o,e))):s.includes(o));case"between":return Array.isArray(s)&&2===s.length&&o>=s[0]&&o<=s[1];case"like":return likeMatch(o,s)}return!1},where=(e=[],{property:r="",joinEntity:t,comparator:s="=",value:a=null,type:o}={})=>e.filter((e=>makeCondition(e,t?`${t}.${r}`:r,s,a,o)));exports.where=where;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.where=void 0,require("core-js/modules/es.array.includes.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.filter.js"),require("core-js/modules/esnext.iterator.some.js");var _compareAs=require("../utilities/compareAs");const likeMatch=(e,r)=>{if("string"!=typeof e||"string"!=typeof r)return!1;const t=r.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/%/g,".*").replace(/_/g,".");return new RegExp(`^${t}$`,"i").test(e)},makeCondition=(e,r,t,s,n=void 0)=>{const a=Object.prototype.hasOwnProperty.call(e,r);switch(t){case"exists":return a;case"does not exist":return!a;case"is null":return a&&null===e[r];case"is not null":return a&&null!==e[r]}if(!a||null===e[r]||null===s)return!1;const o=e[r];switch(t){case"=":return(0,_compareAs.sameValue)(n,o,s);case"!=":case"<>":return!(0,_compareAs.sameValue)(n,o,s);case">":return o>s;case">=":return o>=s;case"<":return o<s;case"<=":return o<=s;case"in":return Array.isArray(s)&&(n?s.some((e=>(0,_compareAs.sameValue)(n,o,e))):s.includes(o));case"between":return Array.isArray(s)&&2===s.length&&!s.includes(null)&&o>=s[0]&&o<=s[1];case"like":return likeMatch(o,s)}return!1},where=(e=[],{property:r="",joinEntity:t,comparator:s="=",value:n=null,type:a}={})=>e.filter((e=>makeCondition(e,t?`${t}.${r}`:r,s,n,a)));exports.where=where;
@@ -8,12 +8,13 @@ export type ParsedCondition = {
8
8
  type?: string;
9
9
  };
10
10
  /**
11
- * Extract condition clauses from the query: `where <field> <comparator> <value>`, each further one joined with
12
- * `and where`. `or where` is refused until or is built (TASKS: the where-clauses epic): it used to be read the same as
11
+ * Extract condition clauses from the query: `where <field> <comparator> <value>`, or `where <field> is null`,
12
+ * `is not null`, `exists` or `does not exist` (no value), each further one joined with `and where`. A comparison
13
+ * with the literal null (`= null`) is refused: it would never match, as null equals nothing (see where). `or where` is refused until or is built (TASKS: the where-clauses epic): it used to be read the same as
13
14
  * `and where`, which silently returned the wrong rows.
14
15
  * @param parsed
15
16
  * @param query
16
- * @throws {Error} For `or where`.
17
+ * @throws {Error} For `or where`, and for a comparison with null.
17
18
  */
18
19
  export declare const findConditions: (parsed: ParsedQuery, query: string) => string;
19
20
  //# sourceMappingURL=findConditions.d.ts.map
@@ -4,18 +4,20 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.findConditions = void 0;
7
+ require("core-js/modules/es.array.includes.js");
7
8
  var _removeFromQuery = require("./removeFromQuery");
8
9
  var _parseLiteralValue = require("./parseLiteralValue");
9
10
  var _maskStrings = require("./maskStrings");
10
11
  // `and`/`or`/`where` count only as whole words: in `on t.x = u.color where ...` the `or` is part of the name `color`
11
- const conditionMatch = new RegExp(`(^|[^a-z0-9._-])(?:(and|or)\\s+)?(where)\\s+([a-z0-9_-]+\\.?[a-z0-9_-]*)\\s+(=|!=|>|<|>=|<=|in|between|like)\\s+(${_maskStrings.placeholderSource}|([0-9]+)|${_maskStrings.arraySource}|(null)|(true)|(false))`, 'ig');
12
+ const conditionMatch = new RegExp(`(^|[^a-z0-9._-])(?:(and|or)\\s+)?(where)\\s+([a-z0-9_-]+\\.?[a-z0-9_-]*)\\s+(?:(is\\s+not\\s+null|is\\s+null|exists|does\\s+not\\s+exist)(?![a-z0-9_.-])|(=|!=|>|<|>=|<=|in|between|like)\\s+(${_maskStrings.placeholderSource}|(${_parseLiteralValue.numberSource})|${_maskStrings.arraySource}|(null)|(true)|(false)))`, 'ig');
12
13
  /**
13
- * Extract condition clauses from the query: `where <field> <comparator> <value>`, each further one joined with
14
- * `and where`. `or where` is refused until or is built (TASKS: the where-clauses epic): it used to be read the same as
14
+ * Extract condition clauses from the query: `where <field> <comparator> <value>`, or `where <field> is null`,
15
+ * `is not null`, `exists` or `does not exist` (no value), each further one joined with `and where`. A comparison
16
+ * with the literal null (`= null`) is refused: it would never match, as null equals nothing (see where). `or where` is refused until or is built (TASKS: the where-clauses epic): it used to be read the same as
15
17
  * `and where`, which silently returned the wrong rows.
16
18
  * @param parsed
17
19
  * @param query
18
- * @throws {Error} For `or where`.
20
+ * @throws {Error} For `or where`, and for a comparison with null.
19
21
  */
20
22
  const findConditions = (parsed, query) => {
21
23
  const conditionsFound = query.matchAll(conditionMatch);
@@ -24,11 +26,26 @@ const findConditions = (parsed, query) => {
24
26
  if (typeof condition[2] !== 'undefined' && condition[2].toLowerCase() === 'or') {
25
27
  throw new Error('`or where` is not supported yet: use `and where`, or run one query per alternative');
26
28
  }
27
- parsed['conditions'].push({
28
- property: condition[4],
29
- comparator: condition[5],
30
- value: (0, _parseLiteralValue.parseLiteralValue)(condition, 7)
31
- });
29
+ if (typeof condition[5] !== 'undefined') {
30
+ parsed['conditions'].push({
31
+ property: condition[4],
32
+ comparator: condition[5].toLowerCase().replace(/\s+/g, ' '),
33
+ value: null
34
+ });
35
+ } else {
36
+ const value = (0, _parseLiteralValue.parseLiteralValue)(condition, 8);
37
+ if (value === null) {
38
+ throw new Error(`\`${condition[4]} ${condition[6]} null\` never matches (null equals nothing, as in SQL): use \`${condition[4]} is null\` or \`${condition[4]} is not null\``);
39
+ }
40
+ if (condition[6].toLowerCase() === 'between' && Array.isArray(value) && value.includes(null)) {
41
+ throw new Error(`\`${condition[4]} between\` can't have a null bound: it would never match`);
42
+ }
43
+ parsed['conditions'].push({
44
+ property: condition[4],
45
+ comparator: condition[6],
46
+ value
47
+ });
48
+ }
32
49
  // Leave out the character before the condition (group 1): it isn't part of it, and an earlier removal may have trimmed it
33
50
  query = (0, _removeFromQuery.removeFromQuery)(condition[0].slice(condition[1].length), query);
34
51
  }
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.findConditions=void 0;var _removeFromQuery=require("./removeFromQuery"),_parseLiteralValue=require("./parseLiteralValue"),_maskStrings=require("./maskStrings");const conditionMatch=new RegExp(`(^|[^a-z0-9._-])(?:(and|or)\\s+)?(where)\\s+([a-z0-9_-]+\\.?[a-z0-9_-]*)\\s+(=|!=|>|<|>=|<=|in|between|like)\\s+(${_maskStrings.placeholderSource}|([0-9]+)|${_maskStrings.arraySource}|(null)|(true)|(false))`,"ig"),findConditions=(e,r)=>{const o=r.matchAll(conditionMatch);for(const t of o){if(void 0!==t[2]&&"or"===t[2].toLowerCase())throw new Error("`or where` is not supported yet: use `and where`, or run one query per alternative");e.conditions.push({property:t[4],comparator:t[5],value:(0,_parseLiteralValue.parseLiteralValue)(t,7)}),r=(0,_removeFromQuery.removeFromQuery)(t[0].slice(t[1].length),r)}return 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");var _removeFromQuery=require("./removeFromQuery"),_parseLiteralValue=require("./parseLiteralValue"),_maskStrings=require("./maskStrings");const conditionMatch=new RegExp(`(^|[^a-z0-9._-])(?:(and|or)\\s+)?(where)\\s+([a-z0-9_-]+\\.?[a-z0-9_-]*)\\s+(?:(is\\s+not\\s+null|is\\s+null|exists|does\\s+not\\s+exist)(?![a-z0-9_.-])|(=|!=|>|<|>=|<=|in|between|like)\\s+(${_maskStrings.placeholderSource}|(${_parseLiteralValue.numberSource})|${_maskStrings.arraySource}|(null)|(true)|(false)))`,"ig"),findConditions=(e,r)=>{const o=r.matchAll(conditionMatch);for(const n of o){if(void 0!==n[2]&&"or"===n[2].toLowerCase())throw new Error("`or where` is not supported yet: use `and where`, or run one query per alternative");if(void 0!==n[5])e.conditions.push({property:n[4],comparator:n[5].toLowerCase().replace(/\s+/g," "),value:null});else{const r=(0,_parseLiteralValue.parseLiteralValue)(n,8);if(null===r)throw new Error(`\`${n[4]} ${n[6]} null\` never matches (null equals nothing, as in SQL): use \`${n[4]} is null\` or \`${n[4]} is not null\``);if("between"===n[6].toLowerCase()&&Array.isArray(r)&&r.includes(null))throw new Error(`\`${n[4]} between\` can't have a null bound: it would never match`);e.conditions.push({property:n[4],comparator:n[6],value:r})}r=(0,_removeFromQuery.removeFromQuery)(n[0].slice(n[1].length),r)}return r};exports.findConditions=findConditions;
@@ -7,9 +7,9 @@ exports.findInsertValues = void 0;
7
7
  var _removeFromQuery = require("./removeFromQuery");
8
8
  var _parseLiteralValue = require("./parseLiteralValue");
9
9
  var _maskStrings = require("./maskStrings");
10
- const literal = `(?:${_maskStrings.placeholderSource.replace('(', '(?:')}|[0-9]+|${_maskStrings.arraySource.replace('(', '(?:')}|null|true|false)`;
10
+ const literal = `(?:${_maskStrings.placeholderSource.replace('(', '(?:')}|${_parseLiteralValue.numberSource}|${_maskStrings.arraySource.replace('(', '(?:')}|null|true|false)`;
11
11
  const valuesClauseMatch = new RegExp(`(values)\\s+([a-z0-9_-]+\\s*=\\s*${literal}(?:\\s*,\\s*[a-z0-9_-]+\\s*=\\s*${literal})*)`, 'i');
12
- const assignmentMatch = new RegExp(`([a-z0-9_-]+)\\s*=\\s*(${_maskStrings.placeholderSource}|([0-9]+)|${_maskStrings.arraySource}|(null)|(true)|(false))`, 'ig');
12
+ const assignmentMatch = new RegExp(`([a-z0-9_-]+)\\s*=\\s*(${_maskStrings.placeholderSource}|(${_parseLiteralValue.numberSource})|${_maskStrings.arraySource}|(null)|(true)|(false))`, 'ig');
13
13
  /**
14
14
  * Extract an insert's "values field = value, field2 = value2" clause from the query, the same way
15
15
  * findUpdateValues() does for "set". insertEntity() accepts an array of rows for bulk inserts, but
@@ -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("(","(?:")}|[0-9]+|${_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}|([0-9]+)|${_maskStrings.arraySource}|(null)|(true)|(false))`,"ig"),findInsertValues=(e,r)=>{const s=r.match(valuesClauseMatch);if(e.insertValues=[],!s)return r;const a={},t=s[2].matchAll(assignmentMatch);for(const e of t)a[e[1]]=(0,_parseLiteralValue.parseLiteralValue)(e,3);return e.insertValues=[a],(0,_removeFromQuery.removeFromQuery)(s[0],r)};exports.findInsertValues=findInsertValues;
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;
@@ -5,7 +5,9 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.findJoinEntity = void 0;
7
7
  var _removeFromQuery = require("./removeFromQuery");
8
- const joinEntityMatch = /\.([a-z0-9_-]+)/i;
8
+ // Only straight after the entity (findCommand leaves `.joined ...`): a `.` later on is a number's (`1.5`) or a
9
+ // condition's `entity.field`, not the join
10
+ const joinEntityMatch = /^\.([a-z0-9_-]+)/i;
9
11
  /**
10
12
  * Extract the main join entity from the query.
11
13
  * @param parsed
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.findJoinEntity=void 0;var _removeFromQuery=require("./removeFromQuery");const joinEntityMatch=/\.([a-z0-9_-]+)/i,findJoinEntity=(t,e)=>{const i=e.match(joinEntityMatch);return i?(t.joinEntity=i?i[1]:void 0,(0,_removeFromQuery.removeFromQuery)(i[0],e)):e};exports.findJoinEntity=findJoinEntity;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.findJoinEntity=void 0;var _removeFromQuery=require("./removeFromQuery");const joinEntityMatch=/^\.([a-z0-9_-]+)/i,findJoinEntity=(t,e)=>{const i=e.match(joinEntityMatch);return i?(t.joinEntity=i?i[1]:void 0,(0,_removeFromQuery.removeFromQuery)(i[0],e)):e};exports.findJoinEntity=findJoinEntity;
@@ -7,9 +7,9 @@ exports.findUpdateValues = void 0;
7
7
  var _removeFromQuery = require("./removeFromQuery");
8
8
  var _parseLiteralValue = require("./parseLiteralValue");
9
9
  var _maskStrings = require("./maskStrings");
10
- const literal = `(?:${_maskStrings.placeholderSource.replace('(', '(?:')}|[0-9]+|${_maskStrings.arraySource.replace('(', '(?:')}|null|true|false)`;
10
+ const literal = `(?:${_maskStrings.placeholderSource.replace('(', '(?:')}|${_parseLiteralValue.numberSource}|${_maskStrings.arraySource.replace('(', '(?:')}|null|true|false)`;
11
11
  const setClauseMatch = new RegExp(`(set)\\s+([a-z0-9_-]+\\s*=\\s*${literal}(?:\\s*,\\s*[a-z0-9_-]+\\s*=\\s*${literal})*)`, 'i');
12
- const assignmentMatch = new RegExp(`([a-z0-9_-]+)\\s*=\\s*(${_maskStrings.placeholderSource}|([0-9]+)|${_maskStrings.arraySource}|(null)|(true)|(false))`, 'ig');
12
+ const assignmentMatch = new RegExp(`([a-z0-9_-]+)\\s*=\\s*(${_maskStrings.placeholderSource}|(${_parseLiteralValue.numberSource})|${_maskStrings.arraySource}|(null)|(true)|(false))`, 'ig');
13
13
  /**
14
14
  * Extract an update's "set field = value, field2 = value2" clause from the query. Uses a two-pass
15
15
  * match, unlike the other parsers here: the set clause's own boundary is found first (so it can
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.findUpdateValues=void 0;var _removeFromQuery=require("./removeFromQuery"),_parseLiteralValue=require("./parseLiteralValue"),_maskStrings=require("./maskStrings");const literal=`(?:${_maskStrings.placeholderSource.replace("(","(?:")}|[0-9]+|${_maskStrings.arraySource.replace("(","(?:")}|null|true|false)`,setClauseMatch=new RegExp(`(set)\\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}|([0-9]+)|${_maskStrings.arraySource}|(null)|(true)|(false))`,"ig"),findUpdateValues=(e,r)=>{const a=r.match(setClauseMatch);if(e.updateValues={},!a)return r;const s=a[2].matchAll(assignmentMatch);for(const r of s)e.updateValues[r[1]]=(0,_parseLiteralValue.parseLiteralValue)(r,3);return(0,_removeFromQuery.removeFromQuery)(a[0],r)};exports.findUpdateValues=findUpdateValues;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.findUpdateValues=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)`,setClauseMatch=new RegExp(`(set)\\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"),findUpdateValues=(e,r)=>{const a=r.match(setClauseMatch);if(e.updateValues={},!a)return r;const s=a[2].matchAll(assignmentMatch);for(const r of s)e.updateValues[r[1]]=(0,_parseLiteralValue.parseLiteralValue)(r,3);return(0,_removeFromQuery.removeFromQuery)(a[0],r)};exports.findUpdateValues=findUpdateValues;
@@ -78,11 +78,15 @@ const unmaskStrings = (text, quote = 'json') => text.replace(placeholderMatch, (
78
78
  exports.unmaskStrings = unmaskStrings;
79
79
  const arrayJson = text => {
80
80
  try {
81
- // A number too big for a double reads as Infinity, which JSON would write back as null: refuse it instead
81
+ // A number too big for a double reads as Infinity, which JSON would write back as null, and a whole number past
82
+ // Number.MAX_SAFE_INTEGER may already be rounded: refuse both, as parseNumber does outside arrays
82
83
  return JSON.stringify(JSON.parse(unmaskStrings(text), (key, value) => {
83
84
  if (typeof value === 'number' && !Number.isFinite(value)) {
84
85
  throw new Error(`${value} is too large`);
85
86
  }
87
+ if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
88
+ throw new Error(`${value} is too large to store exactly (the largest whole number is ${Number.MAX_SAFE_INTEGER}): quote it to keep it as text`);
89
+ }
86
90
  return value;
87
91
  }));
88
92
  } catch (e) {
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.unmaskStrings=exports.placeholderSource=exports.maskStrings=exports.maskArrays=exports.fromPlaceholder=exports.arraySource=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.some.js");var _scanQuery=require("./scanQuery");const placeholderSource=exports.placeholderSource="\\u0000([0-9a-f]*)\\u0000",placeholderMatch=new RegExp(placeholderSource,"g"),arraySource=exports.arraySource="\\u0001([0-9a-f]*)\\u0001",arrayMatch=new RegExp(arraySource,"g"),toHex=r=>r.split("").map((r=>r.charCodeAt(0).toString(16).padStart(4,"0"))).join(""),fromPlaceholder=r=>(r.match(/.{4}/g)||[]).map((r=>String.fromCharCode(parseInt(r,16)))).join("");exports.fromPlaceholder=fromPlaceholder;const maskStrings=r=>{const{strings:e}=(0,_scanQuery.assertClosed)((0,_scanQuery.scanQuery)(r));for(const[s,o]of[["\0","NUL"],["","SOH"]])for(let t=r.indexOf(s);-1!==t;t=r.indexOf(s,t+1))if(!e.some((({start:r,end:e})=>r<t&&t<e)))throw new Error(`Unexpected ${o} character at position ${t}`);return e.reduceRight(((r,{start:e,end:s,value:o})=>`${r.slice(0,e)}\0${toHex(o)}\0${r.slice(s)}`),r)};exports.maskStrings=maskStrings;const unmaskStrings=(r,e="json")=>r.replace(placeholderMatch,((r,s)=>{const o=fromPlaceholder(s);return"json"===e?JSON.stringify(o):`'${o.replace(/\\/g,"\\\\").replace(/'/g,"\\'")}'`})).replace(arrayMatch,((r,e)=>fromPlaceholder(e)));exports.unmaskStrings=unmaskStrings;const arrayJson=r=>{try{return JSON.stringify(JSON.parse(unmaskStrings(r),((r,e)=>{if("number"==typeof e&&!Number.isFinite(e))throw new Error(`${e} is too large`);return e})))}catch(e){throw new Error(`Invalid array ${unmaskStrings(r,"'")}: ${e.message}`)}},maskArrays=r=>{let e="",s=0,o=0;for(let t=0;t<r.length;t++){const a=r[t];if("["===a)0===s&&(o=t),s++;else if("]"===a&&s>0&&(s--,0===s)){e+=`${toHex(arrayJson(r.slice(o,t+1)))}`;continue}0===s&&(e+=a)}return s>0?e+r.slice(o):e};exports.maskArrays=maskArrays;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.unmaskStrings=exports.placeholderSource=exports.maskStrings=exports.maskArrays=exports.fromPlaceholder=exports.arraySource=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.some.js");var _scanQuery=require("./scanQuery");const placeholderSource=exports.placeholderSource="\\u0000([0-9a-f]*)\\u0000",placeholderMatch=new RegExp(placeholderSource,"g"),arraySource=exports.arraySource="\\u0001([0-9a-f]*)\\u0001",arrayMatch=new RegExp(arraySource,"g"),toHex=r=>r.split("").map((r=>r.charCodeAt(0).toString(16).padStart(4,"0"))).join(""),fromPlaceholder=r=>(r.match(/.{4}/g)||[]).map((r=>String.fromCharCode(parseInt(r,16)))).join("");exports.fromPlaceholder=fromPlaceholder;const maskStrings=r=>{const{strings:e}=(0,_scanQuery.assertClosed)((0,_scanQuery.scanQuery)(r));for(const[s,t]of[["\0","NUL"],["","SOH"]])for(let o=r.indexOf(s);-1!==o;o=r.indexOf(s,o+1))if(!e.some((({start:r,end:e})=>r<o&&o<e)))throw new Error(`Unexpected ${t} character at position ${o}`);return e.reduceRight(((r,{start:e,end:s,value:t})=>`${r.slice(0,e)}\0${toHex(t)}\0${r.slice(s)}`),r)};exports.maskStrings=maskStrings;const unmaskStrings=(r,e="json")=>r.replace(placeholderMatch,((r,s)=>{const t=fromPlaceholder(s);return"json"===e?JSON.stringify(t):`'${t.replace(/\\/g,"\\\\").replace(/'/g,"\\'")}'`})).replace(arrayMatch,((r,e)=>fromPlaceholder(e)));exports.unmaskStrings=unmaskStrings;const arrayJson=r=>{try{return JSON.stringify(JSON.parse(unmaskStrings(r),((r,e)=>{if("number"==typeof e&&!Number.isFinite(e))throw new Error(`${e} is too large`);if(Number.isInteger(e)&&!Number.isSafeInteger(e))throw new Error(`${e} is too large to store exactly (the largest whole number is ${Number.MAX_SAFE_INTEGER}): quote it to keep it as text`);return e})))}catch(e){throw new Error(`Invalid array ${unmaskStrings(r,"'")}: ${e.message}`)}},maskArrays=r=>{let e="",s=0,t=0;for(let o=0;o<r.length;o++){const a=r[o];if("["===a)0===s&&(t=o),s++;else if("]"===a&&s>0&&(s--,0===s)){e+=`${toHex(arrayJson(r.slice(t,o+1)))}`;continue}0===s&&(e+=a)}return s>0?e+r.slice(t):e};exports.maskArrays=maskArrays;
@@ -28,11 +28,13 @@ require("core-js/modules/esnext.set.symmetric-difference.js");
28
28
  require("core-js/modules/esnext.set.union.js");
29
29
  var _typeCheck = require("../../introspect/policies/typeCheck");
30
30
  var _maskStrings = require("./maskStrings");
31
+ var _parseLiteralValue = require("./parseLiteralValue");
31
32
  var _startsQuery = require("./startsQuery");
32
33
  const createMatch = exports.createMatch = /^\s*create(?![a-z0-9_-])/i;
33
34
  const headMatch = /^\s*create\s+(if\s+not\s+exists\s+)?([a-z0-9_-]+)\s*\(/i;
34
35
  const nameSource = '[a-z0-9_-]+';
35
36
  const nameMatch = new RegExp(`^${nameSource}$`, 'i');
37
+ const numberMatch = new RegExp(`^${_parseLiteralValue.numberSource}$`, 'i');
36
38
  const fieldMatch = new RegExp(`^(${nameSource})\\s+(${nameSource})((?:\\s[\\s\\S]*)?)$`, 'i');
37
39
  const primaryMatch = /^primary\s+key\s*\(([^()]*)\)$/i;
38
40
  const uniqueMatch = /^unique\s*\(([^()]*)\)$/i;
@@ -123,8 +125,8 @@ const parseDefault = token => {
123
125
  if (array) {
124
126
  return JSON.parse((0, _maskStrings.fromPlaceholder)(array[1]));
125
127
  }
126
- if (/^[0-9]+$/.test(token)) {
127
- return Number(token);
128
+ if (numberMatch.test(token)) {
129
+ return (0, _parseLiteralValue.parseNumber)(token);
128
130
  }
129
131
  switch (token.toLowerCase()) {
130
132
  case 'null':
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.parseCreate=exports.createMatch=void 0,require("core-js/modules/es.array.includes.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.filter.js"),require("core-js/modules/esnext.iterator.find.js"),require("core-js/modules/esnext.iterator.map.js"),require("core-js/modules/esnext.iterator.some.js"),require("core-js/modules/esnext.set.add-all.js"),require("core-js/modules/esnext.set.delete-all.js"),require("core-js/modules/esnext.set.difference.js"),require("core-js/modules/esnext.set.every.js"),require("core-js/modules/esnext.set.filter.js"),require("core-js/modules/esnext.set.find.js"),require("core-js/modules/esnext.set.intersection.js"),require("core-js/modules/esnext.set.is-disjoint-from.js"),require("core-js/modules/esnext.set.is-subset-of.js"),require("core-js/modules/esnext.set.is-superset-of.js"),require("core-js/modules/esnext.set.join.js"),require("core-js/modules/esnext.set.map.js"),require("core-js/modules/esnext.set.reduce.js"),require("core-js/modules/esnext.set.some.js"),require("core-js/modules/esnext.set.symmetric-difference.js"),require("core-js/modules/esnext.set.union.js");var _typeCheck=require("../../introspect/policies/typeCheck"),_maskStrings=require("./maskStrings"),_startsQuery=require("./startsQuery");const createMatch=exports.createMatch=/^\s*create(?![a-z0-9_-])/i,headMatch=/^\s*create\s+(if\s+not\s+exists\s+)?([a-z0-9_-]+)\s*\(/i,nameSource="[a-z0-9_-]+",nameMatch=new RegExp(`^${nameSource}$`,"i"),fieldMatch=new RegExp(`^(${nameSource})\\s+(${nameSource})((?:\\s[\\s\\S]*)?)$`,"i"),primaryMatch=/^primary\s+key\s*\(([^()]*)\)$/i,uniqueMatch=/^unique\s*\(([^()]*)\)$/i,indexMatch=/^index\s*\(([^()]*)\)$/i,foreignMatch=new RegExp(`^foreign\\s+key\\s*\\(([^()]*)\\)\\s*references\\s+(${nameSource})\\s*\\(([^()]*)\\)$`,"i"),stringLiteral=new RegExp(`^${_maskStrings.placeholderSource}$`),arrayLiteral=new RegExp(`^${_maskStrings.arraySource}$`),lookupPrefix={primary:"pk",unique:"unique",index:"index",foreign:"fk"},shown=e=>(0,_maskStrings.unmaskStrings)(e.trim(),"'"),findClose=(e,r)=>{let s=0;for(let t=r;t<e.length;t++)if("("===e[t])s++;else if(")"===e[t]&&(s--,0===s))return t;return-1},splitItems=e=>{const r=[];let s=0,t=0;for(let i=0;i<e.length;i++)"("===e[i]?s++:")"===e[i]?s--:","===e[i]&&0===s&&(r.push(e.slice(t,i)),t=i+1);return r.push(e.slice(t)),r.map((e=>e.trim()))},parseNames=(e,r)=>{const s=e.split(",").map((e=>e.trim()));if(s.some((e=>!nameMatch.test(e))))throw new Error(`Expected field names separated by commas in: ${shown(r)}`);if(new Set(s).size!==s.length)throw new Error(`The same field is named twice in: ${shown(r)}`);return s},parseDefault=e=>{if(void 0===e)throw new Error("Missing a value after default");const r=e.match(stringLiteral);if(r)return(0,_maskStrings.fromPlaceholder)(r[1]);const s=e.match(arrayLiteral);if(s)return JSON.parse((0,_maskStrings.fromPlaceholder)(s[1]));if(/^[0-9]+$/.test(e))return Number(e);switch(e.toLowerCase()){case"null":return null;case"true":return!0;case"false":return!1}throw new Error(`Unrecognized default value: ${shown(e)}`)},parseField=e=>{const[r,s,t,i]=e,n=t.toLowerCase();if(!_typeCheck.fieldTypes.includes(n))throw new Error(`Unknown type "${t}" for field "${s}" (expected ${_typeCheck.fieldTypes.join(", ")})`);const o={name:s,type:n,optional:!1,autoGenerate:!1},a=new Set,l=e=>{if(a.has(e))throw new Error(`"${e}" is given twice for field "${s}"`);a.add(e)},u=""===i.trim()?[]:i.trim().split(/\s+/);for(let e=0;e<u.length;e++){const t=u[e].toLowerCase();if("optional"===t)l("optional"),o.optional=!0;else if("auto"===t)l("auto"),o.autoGenerate=!0;else if("null"===t)l("null"),o.nullable=!0;else if("not"===t&&"null"===(u[e+1]||"").toLowerCase())l("null"),o.nullable=!1,e++;else{if("default"!==t)throw new Error(`Unrecognized "${shown(u[e])}" for field "${s}" in: ${shown(r)}`);l("default"),o.useDefault=!0,o.defaultValue=parseDefault(u[++e])}}if(o.autoGenerate&&o.useDefault)throw new Error(`Field "${s}" can't be both auto and have a default`);if(o.autoGenerate&&"string"!==n)throw new Error(`Field "${s}" is auto, which generates a UUID, so its type must be string`);return o},parseKey=e=>{let r=e.match(primaryMatch);if(r)return{type:"primary",fields:parseNames(r[1],e)};if(r=e.match(uniqueMatch),r)return{type:"unique",fields:parseNames(r[1],e)};if(r=e.match(indexMatch),r)return{type:"index",fields:parseNames(r[1],e)};if(r=e.match(foreignMatch),r){const s=parseNames(r[1],e),t=parseNames(r[3],e);if(1!==s.length||1!==t.length)throw new Error(`Only single-field foreign keys are supported so far: ${shown(e)}`);return{type:"foreign",fields:s,references:[{fields:[`${r[2]}.${t[0]}`],lookup:`${r[2]}.json`}]}}return null},finishKeys=(e,r)=>{const s=e.map((e=>e.name));if(r.filter((e=>"primary"===e.type)).length>1)throw new Error("Only one primary key is allowed");const t=new Set;return r.map((e=>{const r=e.fields.find((e=>!s.includes(e)));if(r)throw new Error(`The ${e.type} key (${e.fields.join(", ")}) names "${r}", which is not a field`);const i=`${lookupPrefix[e.type]}_${e.fields.join("_")}.json`;if(t.has(i))throw new Error(`The same ${e.type} key on "${e.fields.join(", ")}" is given twice`);return t.add(i),Object.assign({type:e.type,fields:e.fields,lookup:i},e.references?{references:e.references}:{})}))},parseCreate=e=>{const r=e.match(headMatch);if(!r)throw new Error("Expected: create [if not exists] <name> (<fields and keys>)");const s=r[2];if("__RECORDS"===s)throw new Error("__RECORDS is reserved for the list of records");const t=r[0].length-1,i=findClose(e,t);if(-1===i)throw new Error("Missing ) at the end of the field list");const n=e.slice(i+1).trim();if(n){if(_startsQuery.startsQueryMatch.test(n))throw new Error(`Missing ; before: ${shown(n)}`);throw new Error(`Unrecognized query value: ${shown(n)}`)}const o=e.slice(t+1,i),a=[],l=[];if(o.trim())for(const e of splitItems(o)){if(!e)throw new Error("Empty item in the field list (an extra comma?)");const r=parseKey(e);if(r){l.push(r);continue}const s=e.match(fieldMatch);if(!s)throw new Error(`Expected a field (<name> <type> ...) or a key in: ${shown(e)}`);const t=parseField(s);if(a.some((e=>e.name===t.name)))throw new Error(`Field "${t.name}" is given twice`);a.push(t)}return{entity:s,create:{ifNotExists:Boolean(r[1]),definition:a,keys:finishKeys(a,l)}}};exports.parseCreate=parseCreate;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.parseCreate=exports.createMatch=void 0,require("core-js/modules/es.array.includes.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.filter.js"),require("core-js/modules/esnext.iterator.find.js"),require("core-js/modules/esnext.iterator.map.js"),require("core-js/modules/esnext.iterator.some.js"),require("core-js/modules/esnext.set.add-all.js"),require("core-js/modules/esnext.set.delete-all.js"),require("core-js/modules/esnext.set.difference.js"),require("core-js/modules/esnext.set.every.js"),require("core-js/modules/esnext.set.filter.js"),require("core-js/modules/esnext.set.find.js"),require("core-js/modules/esnext.set.intersection.js"),require("core-js/modules/esnext.set.is-disjoint-from.js"),require("core-js/modules/esnext.set.is-subset-of.js"),require("core-js/modules/esnext.set.is-superset-of.js"),require("core-js/modules/esnext.set.join.js"),require("core-js/modules/esnext.set.map.js"),require("core-js/modules/esnext.set.reduce.js"),require("core-js/modules/esnext.set.some.js"),require("core-js/modules/esnext.set.symmetric-difference.js"),require("core-js/modules/esnext.set.union.js");var _typeCheck=require("../../introspect/policies/typeCheck"),_maskStrings=require("./maskStrings"),_parseLiteralValue=require("./parseLiteralValue"),_startsQuery=require("./startsQuery");const createMatch=exports.createMatch=/^\s*create(?![a-z0-9_-])/i,headMatch=/^\s*create\s+(if\s+not\s+exists\s+)?([a-z0-9_-]+)\s*\(/i,nameSource="[a-z0-9_-]+",nameMatch=new RegExp(`^${nameSource}$`,"i"),numberMatch=new RegExp(`^${_parseLiteralValue.numberSource}$`,"i"),fieldMatch=new RegExp(`^(${nameSource})\\s+(${nameSource})((?:\\s[\\s\\S]*)?)$`,"i"),primaryMatch=/^primary\s+key\s*\(([^()]*)\)$/i,uniqueMatch=/^unique\s*\(([^()]*)\)$/i,indexMatch=/^index\s*\(([^()]*)\)$/i,foreignMatch=new RegExp(`^foreign\\s+key\\s*\\(([^()]*)\\)\\s*references\\s+(${nameSource})\\s*\\(([^()]*)\\)$`,"i"),stringLiteral=new RegExp(`^${_maskStrings.placeholderSource}$`),arrayLiteral=new RegExp(`^${_maskStrings.arraySource}$`),lookupPrefix={primary:"pk",unique:"unique",index:"index",foreign:"fk"},shown=e=>(0,_maskStrings.unmaskStrings)(e.trim(),"'"),findClose=(e,r)=>{let s=0;for(let t=r;t<e.length;t++)if("("===e[t])s++;else if(")"===e[t]&&(s--,0===s))return t;return-1},splitItems=e=>{const r=[];let s=0,t=0;for(let i=0;i<e.length;i++)"("===e[i]?s++:")"===e[i]?s--:","===e[i]&&0===s&&(r.push(e.slice(t,i)),t=i+1);return r.push(e.slice(t)),r.map((e=>e.trim()))},parseNames=(e,r)=>{const s=e.split(",").map((e=>e.trim()));if(s.some((e=>!nameMatch.test(e))))throw new Error(`Expected field names separated by commas in: ${shown(r)}`);if(new Set(s).size!==s.length)throw new Error(`The same field is named twice in: ${shown(r)}`);return s},parseDefault=e=>{if(void 0===e)throw new Error("Missing a value after default");const r=e.match(stringLiteral);if(r)return(0,_maskStrings.fromPlaceholder)(r[1]);const s=e.match(arrayLiteral);if(s)return JSON.parse((0,_maskStrings.fromPlaceholder)(s[1]));if(numberMatch.test(e))return(0,_parseLiteralValue.parseNumber)(e);switch(e.toLowerCase()){case"null":return null;case"true":return!0;case"false":return!1}throw new Error(`Unrecognized default value: ${shown(e)}`)},parseField=e=>{const[r,s,t,i]=e,n=t.toLowerCase();if(!_typeCheck.fieldTypes.includes(n))throw new Error(`Unknown type "${t}" for field "${s}" (expected ${_typeCheck.fieldTypes.join(", ")})`);const o={name:s,type:n,optional:!1,autoGenerate:!1},a=new Set,l=e=>{if(a.has(e))throw new Error(`"${e}" is given twice for field "${s}"`);a.add(e)},u=""===i.trim()?[]:i.trim().split(/\s+/);for(let e=0;e<u.length;e++){const t=u[e].toLowerCase();if("optional"===t)l("optional"),o.optional=!0;else if("auto"===t)l("auto"),o.autoGenerate=!0;else if("null"===t)l("null"),o.nullable=!0;else if("not"===t&&"null"===(u[e+1]||"").toLowerCase())l("null"),o.nullable=!1,e++;else{if("default"!==t)throw new Error(`Unrecognized "${shown(u[e])}" for field "${s}" in: ${shown(r)}`);l("default"),o.useDefault=!0,o.defaultValue=parseDefault(u[++e])}}if(o.autoGenerate&&o.useDefault)throw new Error(`Field "${s}" can't be both auto and have a default`);if(o.autoGenerate&&"string"!==n)throw new Error(`Field "${s}" is auto, which generates a UUID, so its type must be string`);return o},parseKey=e=>{let r=e.match(primaryMatch);if(r)return{type:"primary",fields:parseNames(r[1],e)};if(r=e.match(uniqueMatch),r)return{type:"unique",fields:parseNames(r[1],e)};if(r=e.match(indexMatch),r)return{type:"index",fields:parseNames(r[1],e)};if(r=e.match(foreignMatch),r){const s=parseNames(r[1],e),t=parseNames(r[3],e);if(1!==s.length||1!==t.length)throw new Error(`Only single-field foreign keys are supported so far: ${shown(e)}`);return{type:"foreign",fields:s,references:[{fields:[`${r[2]}.${t[0]}`],lookup:`${r[2]}.json`}]}}return null},finishKeys=(e,r)=>{const s=e.map((e=>e.name));if(r.filter((e=>"primary"===e.type)).length>1)throw new Error("Only one primary key is allowed");const t=new Set;return r.map((e=>{const r=e.fields.find((e=>!s.includes(e)));if(r)throw new Error(`The ${e.type} key (${e.fields.join(", ")}) names "${r}", which is not a field`);const i=`${lookupPrefix[e.type]}_${e.fields.join("_")}.json`;if(t.has(i))throw new Error(`The same ${e.type} key on "${e.fields.join(", ")}" is given twice`);return t.add(i),Object.assign({type:e.type,fields:e.fields,lookup:i},e.references?{references:e.references}:{})}))},parseCreate=e=>{const r=e.match(headMatch);if(!r)throw new Error("Expected: create [if not exists] <name> (<fields and keys>)");const s=r[2];if("__RECORDS"===s)throw new Error("__RECORDS is reserved for the list of records");const t=r[0].length-1,i=findClose(e,t);if(-1===i)throw new Error("Missing ) at the end of the field list");const n=e.slice(i+1).trim();if(n){if(_startsQuery.startsQueryMatch.test(n))throw new Error(`Missing ; before: ${shown(n)}`);throw new Error(`Unrecognized query value: ${shown(n)}`)}const o=e.slice(t+1,i),a=[],l=[];if(o.trim())for(const e of splitItems(o)){if(!e)throw new Error("Empty item in the field list (an extra comma?)");const r=parseKey(e);if(r){l.push(r);continue}const s=e.match(fieldMatch);if(!s)throw new Error(`Expected a field (<name> <type> ...) or a key in: ${shown(e)}`);const t=parseField(s);if(a.some((e=>e.name===t.name)))throw new Error(`Field "${t.name}" is given twice`);a.push(t)}return{entity:s,create:{ifNotExists:Boolean(r[1]),definition:a,keys:finishKeys(a,l)}}};exports.parseCreate=parseCreate;
@@ -1,4 +1,20 @@
1
1
  export type LiteralValue = string | number | boolean | null | any[];
2
+ /**
3
+ * Regex source matching one number literal: an optional `-`, digits, an optional fraction and an optional exponent
4
+ * (`12`, `-3`, `1.5`, `1e3`, `2.5E-4`), with no group of its own. Anything left after it (`5abc`, `1.5.2`) is an
5
+ * unrecognized query value, as for any other leftover text.
6
+ */
7
+ export declare const numberSource = "-?[0-9]+(?:\\.[0-9]+)?(?:e[+-]?[0-9]+)?";
8
+ /**
9
+ * Read one number literal's text (see numberSource) as a number, refusing any it can't hold exactly as written: a
10
+ * whole number past Number.MAX_SAFE_INTEGER, however it is written (`9007199254740993`, `9007199254740993.0`, `1e20`),
11
+ * may be stored rounded (9007199254740993 as 9007199254740992), and a number too big for a double reads as Infinity,
12
+ * which JSON writes back as null. Fractions are kept as the nearest double, as JSON does (an exact decimal type is
13
+ * planned).
14
+ * @param text
15
+ * @throws {Error} For an integer past Number.MAX_SAFE_INTEGER, or a number too big for a double.
16
+ */
17
+ export declare const parseNumber: (text: string) => number;
2
18
  /**
3
19
  * Several parsers (findConditions, findInsertValues, findUpdateValues) capture a query-string
4
20
  * literal into one of six consecutive alternation groups depending on its shape - quoted string
@@ -3,8 +3,33 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.parseLiteralValue = void 0;
6
+ exports.parseNumber = exports.parseLiteralValue = exports.numberSource = void 0;
7
7
  var _maskStrings = require("./maskStrings");
8
+ /**
9
+ * Regex source matching one number literal: an optional `-`, digits, an optional fraction and an optional exponent
10
+ * (`12`, `-3`, `1.5`, `1e3`, `2.5E-4`), with no group of its own. Anything left after it (`5abc`, `1.5.2`) is an
11
+ * unrecognized query value, as for any other leftover text.
12
+ */
13
+ const numberSource = exports.numberSource = '-?[0-9]+(?:\\.[0-9]+)?(?:e[+-]?[0-9]+)?';
14
+ /**
15
+ * Read one number literal's text (see numberSource) as a number, refusing any it can't hold exactly as written: a
16
+ * whole number past Number.MAX_SAFE_INTEGER, however it is written (`9007199254740993`, `9007199254740993.0`, `1e20`),
17
+ * may be stored rounded (9007199254740993 as 9007199254740992), and a number too big for a double reads as Infinity,
18
+ * which JSON writes back as null. Fractions are kept as the nearest double, as JSON does (an exact decimal type is
19
+ * planned).
20
+ * @param text
21
+ * @throws {Error} For an integer past Number.MAX_SAFE_INTEGER, or a number too big for a double.
22
+ */
23
+ const parseNumber = text => {
24
+ const value = Number(text);
25
+ if (!Number.isFinite(value)) {
26
+ throw new Error(`${text} is too large`);
27
+ }
28
+ if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
29
+ throw new Error(`${text} is too large to store exactly (the largest whole number is ${Number.MAX_SAFE_INTEGER}): quote it to keep it as text`);
30
+ }
31
+ return value;
32
+ };
8
33
  /**
9
34
  * Several parsers (findConditions, findInsertValues, findUpdateValues) capture a query-string
10
35
  * literal into one of six consecutive alternation groups depending on its shape - quoted string
@@ -15,12 +40,13 @@ var _maskStrings = require("./maskStrings");
15
40
  * @param match
16
41
  * @param baseIndex
17
42
  */
43
+ exports.parseNumber = parseNumber;
18
44
  const parseLiteralValue = (match, baseIndex) => {
19
45
  if (typeof match[baseIndex] !== 'undefined') {
20
46
  return (0, _maskStrings.fromPlaceholder)(match[baseIndex]);
21
47
  }
22
48
  if (typeof match[baseIndex + 1] !== 'undefined') {
23
- return Number(match[baseIndex + 1]);
49
+ return parseNumber(match[baseIndex + 1]);
24
50
  }
25
51
  if (typeof match[baseIndex + 2] !== 'undefined') {
26
52
  // An array is a placeholder from maskArrays, holding its JSON
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.parseLiteralValue=void 0;var _maskStrings=require("./maskStrings");const parseLiteralValue=(e,r)=>void 0!==e[r]?(0,_maskStrings.fromPlaceholder)(e[r]):void 0!==e[r+1]?Number(e[r+1]):void 0!==e[r+2]?JSON.parse((0,_maskStrings.fromPlaceholder)(e[r+2])):void 0!==e[r+3]?null:void 0!==e[r+4];exports.parseLiteralValue=parseLiteralValue;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.parseNumber=exports.parseLiteralValue=exports.numberSource=void 0;var _maskStrings=require("./maskStrings");const numberSource=exports.numberSource="-?[0-9]+(?:\\.[0-9]+)?(?:e[+-]?[0-9]+)?",parseNumber=e=>{const r=Number(e);if(!Number.isFinite(r))throw new Error(`${e} is too large`);if(Number.isInteger(r)&&!Number.isSafeInteger(r))throw new Error(`${e} is too large to store exactly (the largest whole number is ${Number.MAX_SAFE_INTEGER}): quote it to keep it as text`);return r};exports.parseNumber=parseNumber;const parseLiteralValue=(e,r)=>void 0!==e[r]?(0,_maskStrings.fromPlaceholder)(e[r]):void 0!==e[r+1]?parseNumber(e[r+1]):void 0!==e[r+2]?JSON.parse((0,_maskStrings.fromPlaceholder)(e[r+2])):void 0!==e[r+3]?null:void 0!==e[r+4];exports.parseLiteralValue=parseLiteralValue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-fs-query",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Use json files to manage and read data.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",