wizard-ql 3.0.0 → 4.0.1

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.
package/README.md CHANGED
@@ -20,6 +20,9 @@
20
20
 
21
21
  [Demo](https://exoRift.github.io/wizard-ql)
22
22
 
23
+ ## Installation
24
+ `npm i wizard-ql`
25
+
23
26
  ## Examples
24
27
  `WizardParser.parse('(rank >= 10 & role = admin) | banned')`
25
28
 
@@ -327,13 +330,13 @@ A record mapping field names to (`boolean`, `string`, `number`, `date`). The val
327
330
  > ```
328
331
 
329
332
  > [!NOTE]
330
- > A value will only be attempted to be parsed as a date if `'date'` is included in the field's type record.
331
- > If the value is a number, it will be parsed as number of milliseconds. Any non-number character will cause the value to be parsed as a string
333
+ > A value will only be attempted to be parsed as a date if `'date'` is included in the field's type record or if the operator is exclusively a date operator.
334
+ > According to the default date parser, if the value is a number, it will be parsed as number of milliseconds. Any non-number character will cause the value to be parsed as a string (See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date)
332
335
 
333
336
  The type coercion priority chain is as follows:
334
337
  `boolean` -> `date` -> `number` -> `string`
335
338
  > [!WARNING]
336
- > This can cause issues for numbers with leading zeros. If this is a potential problem, make sure to exclude `number` from the types and include `string`
339
+ > This can cause issues for numbers with leading zeros. If this is a potential problem, make sure to exclude `number` from the field types and include `string`
337
340
 
338
341
  ### Regarding constraints: `validated` property
339
342
  When a field has matched either a key in `types` or a field in `restricted`, the `validated` property on the parsed condition will be true. This is due to a limitation with TypeScript's type checking.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export * from './spec';
2
2
  export * from './parser';
3
- export { executeAsKnex } from './execute';
4
3
  export { ParseError, ConstraintError } from './errors';
5
4
  export { createDOMInput } from './dominput';
package/dist/index.js CHANGED
@@ -185,14 +185,14 @@ class WizardParser {
185
185
  NOTMATCH: "≇"
186
186
  }
187
187
  };
188
- static QUOTES = ["'", '"', "`"];
189
- static NEGATORS = ["!"];
190
- static ARRAY_DELIMITERS = [","];
191
- static ARRAY_BRACKETS = [
188
+ static DEFAULT_QUOTES = ['"', "'", "`"];
189
+ static DEFAULT_NEGATORS = ["!"];
190
+ static DEFAULT_ARRAY_DELIMITERS = [","];
191
+ static DEFAULT_ARRAY_BRACKETS = [
192
192
  ["[", "]"],
193
193
  ["{", "}"]
194
194
  ];
195
- static GROUP_BRACKETS = [
195
+ static DEFAULT_GROUP_BRACKETS = [
196
196
  ["(", ")"]
197
197
  ];
198
198
  OPERATOR_DICTIONARY;
@@ -216,6 +216,38 @@ class WizardParser {
216
216
  if (!config.dialects)
217
217
  config.dialects = WizardParser.DEFAULT_DIALECTS;
218
218
  }
219
+ const symbolPool = new Set;
220
+ config.symbols ??= {};
221
+ config.symbols.quotes ??= WizardParser.DEFAULT_QUOTES;
222
+ config.symbols.negators ??= WizardParser.DEFAULT_NEGATORS;
223
+ config.symbols.arrayDelimiters ??= WizardParser.DEFAULT_ARRAY_DELIMITERS;
224
+ config.symbols.arrayBrackets ??= WizardParser.DEFAULT_ARRAY_BRACKETS;
225
+ config.symbols.groupBrackets ??= WizardParser.DEFAULT_GROUP_BRACKETS;
226
+ for (const [opening, closing] of config.symbols.arrayBrackets) {
227
+ if (opening === closing)
228
+ throw Error(`Opening array bracket cannot be the same as closing array bracket ("${opening}")`);
229
+ if (symbolPool.has(opening))
230
+ throw Error(`Opening array bracket "${opening}" uses a symbol already defined elsewhere in the parser`);
231
+ if (symbolPool.has(closing))
232
+ throw Error(`Closing array bracket "${closing}" uses a symbol already defined elsewhere in the parser`);
233
+ symbolPool.add(opening);
234
+ symbolPool.add(closing);
235
+ }
236
+ for (const [opening, closing] of config.symbols.groupBrackets) {
237
+ if (opening === closing)
238
+ throw Error(`Opening group bracket cannot be the same as closing group bracket ("${opening}")`);
239
+ if (symbolPool.has(opening))
240
+ throw Error(`Opening group bracket "${opening}" uses a symbol already defined elsewhere in the parser`);
241
+ if (symbolPool.has(closing))
242
+ throw Error(`Closing group bracket "${closing}" uses a symbol already defined elsewhere in the parser`);
243
+ symbolPool.add(opening);
244
+ symbolPool.add(closing);
245
+ }
246
+ for (const quote of config.symbols.quotes) {
247
+ if (symbolPool.has(quote))
248
+ throw Error(`Quote "${quote}" uses a symbol already defined elsewhere in the parser`);
249
+ symbolPool.add(quote);
250
+ }
219
251
  this.FIELD_LIST = new Map;
220
252
  if (config.caseInsensitive) {
221
253
  if (config.types) {
@@ -230,6 +262,24 @@ class WizardParser {
230
262
  this.OPERATOR_DICTIONARY = {};
231
263
  for (const operationName in config.operators) {
232
264
  const operation = config.operators[operationName];
265
+ for (const symbol of symbolPool) {
266
+ if (operationName.includes(symbol))
267
+ throw Error(`Operator "${operationName}" contains symbol "${symbol}"`);
268
+ if (operation.aliases) {
269
+ for (const alias of operation.aliases) {
270
+ if (alias.includes(symbol))
271
+ throw Error(`Operator "${operationName}" alias "${alias}" contains symbol "${symbol}"`);
272
+ }
273
+ }
274
+ if (operation.negationName.includes(symbol))
275
+ throw Error(`Operator "${operation.negationName}" contains symbol "${symbol}"`);
276
+ if (operation.negationAliases) {
277
+ for (const alias of operation.negationAliases) {
278
+ if (alias.includes(symbol))
279
+ throw Error(`Operator "${operation.negationName}" alias "${alias}" contains symbol "${symbol}"`);
280
+ }
281
+ }
282
+ }
233
283
  const opDef = {
234
284
  name: operationName,
235
285
  type: operation.type,
@@ -263,11 +313,16 @@ class WizardParser {
263
313
  }
264
314
  }
265
315
  }
266
- this.TOKEN_REGEX = new RegExp(createTokenRegexString(Object.keys(this.OPERATOR_DICTIONARY).concat(WizardParser.GROUP_BRACKETS.flat()).concat(WizardParser.ARRAY_BRACKETS.flat()).concat(WizardParser.NEGATORS).concat(WizardParser.ARRAY_DELIMITERS), WizardParser.QUOTES), "g");
267
- const quoteRegexStr = createQuoteRegexString(WizardParser.QUOTES);
316
+ for (const negator of config.symbols.negators) {
317
+ if (symbolPool.has(negator))
318
+ throw Error(`Negator "${negator}" uses a symbol already defined elsewhere in the parser`);
319
+ symbolPool.add(negator);
320
+ }
321
+ this.TOKEN_REGEX = new RegExp(createTokenRegexString(Object.keys(this.OPERATOR_DICTIONARY).concat(config.symbols.groupBrackets.flat()).concat(config.symbols.arrayBrackets.flat()).concat(config.symbols.negators).concat(config.symbols.arrayDelimiters), config.symbols.quotes), "g");
322
+ const quoteRegexStr = createQuoteRegexString(config.symbols.quotes);
268
323
  this.QUOTE_REGEX = new RegExp(quoteRegexStr);
269
324
  this.QUOTE_EDGE_REGEX = new RegExp(`^${quoteRegexStr}$`);
270
- this.ARRAY_DELIMITER_REGEX = new RegExp(createArrayDelimitRegexString(WizardParser.QUOTES, WizardParser.ARRAY_DELIMITERS), "g");
325
+ this.ARRAY_DELIMITER_REGEX = new RegExp(createArrayDelimitRegexString(config.symbols.quotes, config.symbols.arrayDelimiters), "g");
271
326
  Object.freeze(config);
272
327
  }
273
328
  static pushSanitized(array, item, index) {
@@ -293,7 +348,7 @@ class WizardParser {
293
348
  let lastMatchEnd = null;
294
349
  for (const match of matches) {
295
350
  WizardParser.pushSanitized(tokens, expression.slice(lastMatchEnd ?? 0, match.index), lastMatchEnd === null ? 0 : lastMatchEnd);
296
- if (WizardParser.ARRAY_BRACKETS.some(([o]) => match[0] === o)) {
351
+ if (this.CONFIG.symbols.arrayBrackets.some(([o]) => match[0] === o)) {
297
352
  const startToken = {
298
353
  content: match[0],
299
354
  index: match.index
@@ -304,7 +359,7 @@ class WizardParser {
304
359
  for (const submatch of matches) {
305
360
  if (match[0] === submatch[0] || match[0] === submatch[0])
306
361
  ++subopenings;
307
- if (WizardParser.ARRAY_BRACKETS.some(([o, c]) => match[0] === o && submatch[0] === c)) {
362
+ if (this.CONFIG.symbols.arrayBrackets.some(([o, c]) => match[0] === o && submatch[0] === c)) {
308
363
  if (subopenings)
309
364
  --subopenings;
310
365
  else {
@@ -419,7 +474,7 @@ class WizardParser {
419
474
  throw new Error("operator");
420
475
  if (!fieldHits)
421
476
  throw new Error("field");
422
- throw new Error("operator");
477
+ throw new Error("disagreement");
423
478
  }
424
479
  validateCondition(condition, valueIsImplicit, ctx) {
425
480
  let validated = false;
@@ -505,7 +560,9 @@ class WizardParser {
505
560
  case "operator":
506
561
  throw new ConstraintError(`Value "${condition.value.toString()}" not allowed for operation "${condition.operation}" which only allows for "${operationType}" type`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
507
562
  case "field":
508
- throw new ConstraintError(`Value "${condition.value.toString()}" includes a type not permitted for field "${condition.field}". Allowed types: ${(fieldTypes ?? opTypes).join(", ")}`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
563
+ throw new ConstraintError(`Value "${condition.value.toString()}" only has types not permitted for field "${condition.field}". Allowed types: ${(fieldTypes ?? opTypes).join(", ")}`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
564
+ case "disagreement":
565
+ throw new ConstraintError(`Value "${condition.value.toString()}" using operation "${condition.operation}" cannot be coerced into a valid type for "${condition.field}". Allowed field types: ${(fieldTypes ?? ["ALL"]).join(", ")}; Allowed operator types: ${opTypes.join(", ")}`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
509
566
  }
510
567
  }
511
568
  }
@@ -579,11 +636,11 @@ class WizardParser {
579
636
  };
580
637
  for (let t = 0;t < tokens.length; ++t) {
581
638
  const token = tokens[t];
582
- if (WizardParser.GROUP_BRACKETS.some(([, c]) => token.content === c))
639
+ if (this.CONFIG.symbols.groupBrackets.some(([, c]) => token.content === c))
583
640
  throw new ParseError("Unexpected closing parenthesis", tokens, token, _offset + t);
584
- if (WizardParser.ARRAY_BRACKETS.some(([, c]) => token.content === c))
641
+ if (this.CONFIG.symbols.arrayBrackets.some(([, c]) => token.content === c))
585
642
  throw new ParseError("Unexpected closing bracket/brace", tokens, token, _offset + t);
586
- const paren = WizardParser.GROUP_BRACKETS.find(([o]) => token.content === o);
643
+ const paren = this.CONFIG.symbols.groupBrackets.find(([o]) => token.content === o);
587
644
  if (paren) {
588
645
  if (field || comparisonOperation || value)
589
646
  throw new ParseError("Tried to open a group during an operation", tokens, token, _offset + t);
@@ -665,9 +722,9 @@ class WizardParser {
665
722
  }
666
723
  continue;
667
724
  }
668
- if (WizardParser.NEGATORS.includes(token.content)) {
725
+ if (this.CONFIG.symbols.negators.includes(token.content)) {
669
726
  const nextToken = tokens[t + 1];
670
- const nextParen = WizardParser.GROUP_BRACKETS.find(([o]) => nextToken?.content === o);
727
+ const nextParen = this.CONFIG.symbols.groupBrackets.find(([o]) => nextToken?.content === o);
671
728
  if (nextParen) {
672
729
  resolveCondition({
673
730
  tokens,
@@ -716,7 +773,7 @@ class WizardParser {
716
773
  throw new ParseError("Expected a comparison operator", tokens, field.token, field.index, token, _offset + t);
717
774
  }
718
775
  if (!field) {
719
- if (WizardParser.NEGATORS.includes(token.content)) {
776
+ if (this.CONFIG.symbols.negators.includes(token.content)) {
720
777
  if (!this.CONFIG.implicitCondition)
721
778
  throw new ParseError("Could not attempt a negative implicit condition as one is not defined in the config", tokens, token, _offset + t);
722
779
  const nextToken = tokens[t + 1];
@@ -759,7 +816,7 @@ class WizardParser {
759
816
  continue;
760
817
  }
761
818
  if (!value) {
762
- const bracket = WizardParser.ARRAY_BRACKETS.find(([o]) => token.content === o);
819
+ const bracket = this.CONFIG.symbols.arrayBrackets.find(([o]) => token.content === o);
763
820
  if (bracket) {
764
821
  const closingIndex = WizardParser.getClosingIndex(tokens, t, bracket[0], bracket[1]);
765
822
  if (closingIndex === -1)
@@ -792,7 +849,7 @@ class WizardParser {
792
849
  };
793
850
  for (let ct = 0;ct < arrayContents.length; ++ct) {
794
851
  const contentToken = arrayContents[ct];
795
- if (WizardParser.ARRAY_DELIMITERS.includes(contentToken.content)) {
852
+ if (this.CONFIG.symbols.arrayDelimiters.includes(contentToken.content)) {
796
853
  if (!workingEntry)
797
854
  throw new ParseError("Unexpected blank entry in array", tokens, contentToken, _offset + t + ct);
798
855
  resolveEntry(contentToken, _offset + t + ct);
@@ -867,20 +924,21 @@ class WizardParser {
867
924
  return this._parse(tokens, 0);
868
925
  }
869
926
  addQuotesIfNecessary(value) {
927
+ const QUOTE = this.CONFIG.symbols.quotes[0];
870
928
  if (value instanceof Date) {
871
- return this.CONFIG.dateSerializer ? this.CONFIG.dateSerializer(value) : value.toISOString();
929
+ return this.CONFIG.dateSerializer ? this.CONFIG.dateSerializer(value) : QUOTE + value.toISOString() + QUOTE;
872
930
  }
873
931
  if (typeof value !== "string")
874
932
  return value.toString();
875
933
  if (value === "true")
876
- return '"true"';
934
+ return `${QUOTE}true${QUOTE}`;
877
935
  if (value === "false")
878
- return '"false"';
936
+ return `${QUOTE}false${QUOTE}`;
879
937
  if (!isNaN(Number(value)))
880
- return `"${value}"`;
938
+ return `${QUOTE}${value}${QUOTE}`;
881
939
  const escaped = value.replaceAll("\\", "\\\\");
882
940
  if (new RegExp(this.TOKEN_REGEX, "gi").test(escaped))
883
- return `"${escaped.replaceAll('"', "\\\"")}"`;
941
+ return `${QUOTE}${escaped.replaceAll(QUOTE, `\\${QUOTE}`)}${QUOTE}`;
884
942
  return escaped;
885
943
  }
886
944
  stringify(expression, opts) {
@@ -908,25 +966,25 @@ class WizardParser {
908
966
  string += " ";
909
967
  }
910
968
  if (alwaysParenthesize && constituent.type === "group" || expression.operation === "AND" && constituent.operation === "OR")
911
- string += WizardParser.GROUP_BRACKETS[0][0];
969
+ string += this.CONFIG.symbols.groupBrackets[0][0];
912
970
  string += this.stringify(constituent, opts);
913
971
  if (alwaysParenthesize && constituent.type === "group" || expression.operation === "AND" && constituent.operation === "OR")
914
- string += WizardParser.GROUP_BRACKETS[0][1];
972
+ string += this.CONFIG.symbols.groupBrackets[0][1];
915
973
  }
916
974
  break;
917
975
  case "condition": {
918
976
  const resolveNonImplicit = () => {
919
977
  const transformed = this.CONFIG.dialects[dialect][expression.operation];
920
978
  const alpha = isAlpha(transformed);
921
- string += expression.field;
979
+ string += expression.field.toString();
922
980
  if (!compact || alpha)
923
981
  string += " ";
924
982
  string += transformed;
925
983
  if (!compact || alpha)
926
984
  string += " ";
927
985
  if (Array.isArray(expression.value)) {
928
- const join = expression.value.map((v) => this.addQuotesIfNecessary(v)).join(compact ? WizardParser.ARRAY_DELIMITERS[0] : WizardParser.ARRAY_DELIMITERS[0] + " ");
929
- string += `${WizardParser.ARRAY_BRACKETS[0][0]}${join}${WizardParser.ARRAY_BRACKETS[0][1]}`;
986
+ const join = expression.value.map((v) => this.addQuotesIfNecessary(v)).join(compact ? this.CONFIG.symbols.arrayDelimiters[0] : this.CONFIG.symbols.arrayDelimiters[0] + " ");
987
+ string += `${this.CONFIG.symbols.arrayBrackets[0][0]}${join}${this.CONFIG.symbols.arrayBrackets[0][1]}`;
930
988
  } else
931
989
  string += this.addQuotesIfNecessary(expression.value);
932
990
  };
@@ -938,12 +996,18 @@ class WizardParser {
938
996
  const fieldTypes = fieldType && (Array.isArray(fieldType) ? fieldType : [fieldType]);
939
997
  const opTypes = Array.isArray(this.CONFIG.implicitCondition.asType) ? this.CONFIG.implicitCondition.asType : [this.CONFIG.implicitCondition.asType];
940
998
  const processed = this.processToken(this.CONFIG.implicitCondition.value);
941
- const coerced = this.coerceType(processed, fieldTypes ?? opTypes, opTypes);
999
+ let coerced;
1000
+ try {
1001
+ coerced = this.coerceType(processed, fieldTypes ?? opTypes, opTypes);
1002
+ } catch {
1003
+ resolveNonImplicit();
1004
+ break;
1005
+ }
942
1006
  const bothBoolean = typeof coerced === "boolean" && typeof expression.value === "boolean";
943
1007
  if (coerced.valueOf() === expression.value || bothBoolean) {
944
1008
  if (bothBoolean && coerced !== expression.value)
945
1009
  isNegation = !isNegation;
946
- string += `${isNegation ? WizardParser.NEGATORS[0] : ""}${expression.field}`;
1010
+ string += `${isNegation ? this.CONFIG.symbols.negators[0] : ""}${expression.field.toString()}`;
947
1011
  } else
948
1012
  resolveNonImplicit();
949
1013
  } else
@@ -991,23 +1055,23 @@ class WizardParser {
991
1055
  return "quoted";
992
1056
  if (!isNaN(Number(segment)))
993
1057
  return "number";
994
- for (const [opening, closing] of WizardParser.ARRAY_BRACKETS) {
1058
+ for (const [opening, closing] of this.CONFIG.symbols.arrayBrackets) {
995
1059
  if (!activeArrayOpeningBracket && segment === opening)
996
1060
  return "openingarraybracket";
997
1061
  else if (activeArrayOpeningBracket === opening && segment === closing)
998
1062
  return "closingarraybracket";
999
1063
  }
1000
1064
  if (!activeArrayOpeningBracket) {
1001
- for (const [opening, closing] of WizardParser.GROUP_BRACKETS) {
1065
+ for (const [opening, closing] of this.CONFIG.symbols.groupBrackets) {
1002
1066
  if (segment === opening)
1003
1067
  return "openinggroupbracket";
1004
1068
  else if (segment === closing)
1005
1069
  return "closinggroupbracket";
1006
1070
  }
1007
1071
  }
1008
- if (activeArrayOpeningBracket && WizardParser.ARRAY_DELIMITERS.includes(segment))
1072
+ if (activeArrayOpeningBracket && this.CONFIG.symbols.arrayDelimiters.includes(segment))
1009
1073
  return "arraydelimiter";
1010
- if (!activeArrayOpeningBracket && WizardParser.NEGATORS.includes(segment))
1074
+ if (!activeArrayOpeningBracket && this.CONFIG.symbols.negators.includes(segment))
1011
1075
  return "negator";
1012
1076
  if (!activeArrayOpeningBracket && segment in this.OPERATOR_DICTIONARY) {
1013
1077
  const type = this.OPERATOR_DICTIONARY[segment].type;
@@ -1022,52 +1086,6 @@ class WizardParser {
1022
1086
  return this.OPERATOR_DICTIONARY[alias]?.name;
1023
1087
  }
1024
1088
  }
1025
- // src/execute.ts
1026
- function executeAsKnex(query, expression) {
1027
- switch (expression.type) {
1028
- case "group": {
1029
- let firstHappened = false;
1030
- for (const constituent of expression.constituents) {
1031
- query[firstHappened ? expression.operation === "AND" ? "andWhere" : "orWhere" : "where"]((clause) => executeAsKnex(clause, constituent));
1032
- firstHappened = true;
1033
- }
1034
- break;
1035
- }
1036
- case "condition":
1037
- switch (expression.operation) {
1038
- case "EQUAL":
1039
- query.where(expression.field, "=", expression.value);
1040
- break;
1041
- case "NOTEQUAL":
1042
- query.where(expression.field, "!=", expression.value);
1043
- break;
1044
- case "GEQ":
1045
- query.where(expression.field, ">=", expression.value);
1046
- break;
1047
- case "LEQ":
1048
- query.where(expression.field, "<=", expression.value);
1049
- break;
1050
- case "GREATER":
1051
- query.where(expression.field, ">", expression.value);
1052
- break;
1053
- case "LESS":
1054
- query.where(expression.field, "<", expression.value);
1055
- break;
1056
- case "IN":
1057
- query.whereIn(expression.field, expression.value);
1058
- break;
1059
- case "NOTIN":
1060
- query.whereNotIn(expression.field, expression.value);
1061
- break;
1062
- case "MATCH":
1063
- query.whereRaw("?? ~* ?", [expression.field, expression.value]);
1064
- break;
1065
- case "NOTMATCH":
1066
- query.whereRaw("?? !~* ?", [expression.field, expression.value]);
1067
- break;
1068
- }
1069
- }
1070
- }
1071
1089
  // src/dominput.ts
1072
1090
  function getCursorIndex(element) {
1073
1091
  const selection = window.getSelection();
@@ -1259,10 +1277,9 @@ function createDOMInput({ input, config, onUpdate, parseOnInitialize }) {
1259
1277
  };
1260
1278
  }
1261
1279
  export {
1262
- executeAsKnex,
1263
- createDOMInput,
1264
- WizardParser,
1265
- TYPE_PRIORITY,
1280
+ ConstraintError,
1266
1281
  ParseError,
1267
- ConstraintError
1282
+ TYPE_PRIORITY,
1283
+ WizardParser,
1284
+ createDOMInput
1268
1285
  };
package/dist/parser.d.ts CHANGED
@@ -58,6 +58,7 @@ type ValidateUppercaseKeys<O extends OperatorRecord> = {
58
58
  [K in keyof O]: K extends Uppercase<K & string> ? O[K] : ValidationError<`Operator Record contains non-uppercase key: '${K & string}'`>;
59
59
  };
60
60
  type InternalOperationDictionary<O extends OperatorRecord> = Record<GetConditionOperators<O> | GetJunctionOperators<O> | string, InternalConditionOperatorDefinition<O> | InternalJunctionOperatorDefinition<O>>;
61
+ type ReadonlyNonemptyArray<T> = readonly [T, ...T[]];
61
62
  export interface WizardParserConfig<F extends FieldTypeRecord, O extends OperatorRecord, out V extends boolean, D extends string> {
62
63
  /**
63
64
  * Restricted fields.\
@@ -69,7 +70,7 @@ export interface WizardParserConfig<F extends FieldTypeRecord, O extends Operato
69
70
  * If the query value is of array type, it will check all entries of the array and ensure they're all allowed.\
70
71
  * This check runs before type coercion so the value checked will always be a string. However, quotes and escapes WILL be removed
71
72
  */
72
- restricted?: Partial<Record<keyof F | (string & {}), boolean | ['allow' | 'deny', Array<string | RegExp>]>>;
73
+ restricted?: Partial<Record<keyof F | (string & {}), boolean | ['allow' | 'deny', ReadonlyArray<string | RegExp>]>>;
73
74
  /**
74
75
  * The types of fields\
75
76
  * Either provide the field type singularly or permit multiple types with an array of field types\
@@ -111,9 +112,24 @@ export interface WizardParserConfig<F extends FieldTypeRecord, O extends Operato
111
112
  /** The default implicit value in string form */
112
113
  value: string;
113
114
  /** How the value should be parsed (as a type) */
114
- asType: FieldType | FieldType[];
115
+ asType: FieldType | readonly FieldType[];
115
116
  };
117
+ /**
118
+ * A specification of how to stringify a parsed expression.\
119
+ * Provide a record that maps dialect names to a record which maps operator names to their stringified expressions
120
+ */
116
121
  dialects?: Record<D, Record<GetOperators<O>, string>>;
122
+ /**
123
+ * Symbol overrides.\
124
+ * Stringification will use the first symbol entry of each symbol type.
125
+ */
126
+ symbols?: {
127
+ quotes?: ReadonlyNonemptyArray<string>;
128
+ negators?: ReadonlyNonemptyArray<string>;
129
+ arrayDelimiters?: ReadonlyNonemptyArray<string>;
130
+ arrayBrackets?: ReadonlyNonemptyArray<[opening: string, closing: string]>;
131
+ groupBrackets?: ReadonlyNonemptyArray<[opening: string, closing: string]>;
132
+ };
117
133
  }
118
134
  export interface StringifyOptions<D extends string> {
119
135
  /**
@@ -237,13 +253,15 @@ export declare class WizardParser<const F extends FieldTypeRecord, const O exten
237
253
  readonly NOTMATCH: "≇";
238
254
  };
239
255
  };
240
- protected static readonly QUOTES: readonly ["'", "\"", "`"];
241
- protected static readonly NEGATORS: readonly ["!"];
242
- protected static readonly ARRAY_DELIMITERS: readonly [","];
243
- protected static readonly ARRAY_BRACKETS: [["[", "]"], ["{", "}"]];
244
- protected static readonly GROUP_BRACKETS: [["(", ")"]];
256
+ static readonly DEFAULT_QUOTES: readonly ["\"", "'", "`"];
257
+ static readonly DEFAULT_NEGATORS: readonly ["!"];
258
+ static readonly DEFAULT_ARRAY_DELIMITERS: readonly [","];
259
+ static readonly DEFAULT_ARRAY_BRACKETS: [["[", "]"], ["{", "}"]];
260
+ static readonly DEFAULT_GROUP_BRACKETS: [["(", ")"]];
245
261
  protected readonly OPERATOR_DICTIONARY: InternalOperationDictionary<O>;
246
- protected readonly CONFIG: WizardParserConfig<F, O, V, D> & Required<Pick<WizardParserConfig<F, O, V, D>, 'operators'>>;
262
+ protected readonly CONFIG: WizardParserConfig<F, O, V, D> & Required<Pick<WizardParserConfig<F, O, V, D>, 'operators'>> & {
263
+ symbols: Required<Exclude<WizardParserConfig<F, O, V, D>['symbols'], undefined>>;
264
+ };
247
265
  protected readonly TOKEN_REGEX: RegExp;
248
266
  protected readonly QUOTE_REGEX: RegExp;
249
267
  protected readonly QUOTE_EDGE_REGEX: RegExp;
@@ -310,7 +328,7 @@ export declare class WizardParser<const F extends FieldTypeRecord, const O exten
310
328
  * @param fieldTypes The supported field types
311
329
  * @param operatorTypes The supported operator types
312
330
  * @returns The coerced type
313
- * @throws {Error} Message is 'operator' if the operator type cannot be coerced and 'field' if the field type cannot be coerced
331
+ * @throws {Error} Message is 'operator' if the operator type cannot be coerced, 'field' if the field type cannot be coerced and 'disagreement' if the allowed types for the field and operator have no overlap
314
332
  */
315
333
  protected coerceType(processedToken: ProcessedToken, fieldTypes: FieldType[], operatorTypes: FieldType[]): Primitive;
316
334
  /**
@@ -353,7 +371,7 @@ export declare class WizardParser<const F extends FieldTypeRecord, const O exten
353
371
  * @throws {Error} If no dialect dictionary is defined or dialect supplied is unknown
354
372
  * @returns The formatted string
355
373
  */
356
- stringify(expression: Expression<FieldTypeRecord, O>, opts: D | StringifyOptions<D>): string;
374
+ stringify(expression: Expression<F, O>, opts: D | StringifyOptions<D>): string;
357
375
  /**
358
376
  * (Get an array entry from a map or insert one) and return it
359
377
  * @param map The map
package/dist/spec.d.ts CHANGED
@@ -51,18 +51,31 @@ export type GetConditionTSType<T extends ConditionOperatorComparisonType> = {
51
51
  numeric: number | Date;
52
52
  array: Primitive[];
53
53
  }[T];
54
+ /**
55
+ * The TS type of a condition value belonging to a field with no declared types\
56
+ * Values are only parsed into dates for fields declared as dates, so a date can only occur when the operator itself is exclusively date-typed
57
+ */
58
+ export type GetUndeclaredConditionTSType<T extends ConditionOperatorComparisonType> = {
59
+ primitive: Exclude<Primitive, Date>;
60
+ boolean: boolean;
61
+ string: string;
62
+ number: number;
63
+ date: Date;
64
+ numeric: number;
65
+ array: Array<Exclude<Primitive, Date>>;
66
+ }[T];
54
67
  export interface CheckedCondition<F extends FieldTypeRecord = FieldTypeRecord, O extends OperatorRecord = OperatorRecord, I extends keyof F = keyof F, P extends GetConditionOperators<O> = GetConditionOperators<O>> {
55
68
  type: 'condition';
56
69
  operation: P;
57
70
  field: I;
58
- value: GetOperatorDefinition<O, P>['type'] extends 'array' ? Array<GetFieldTSType<F[I]>> : Extract<GetConditionTSType<GetOperatorDefinition<O, P>['type'] & ConditionOperatorComparisonType>, GetFieldTSType<F[I]>>;
71
+ value: string extends I ? GetUndeclaredConditionTSType<GetOperatorDefinition<O, P>['type'] & ConditionOperatorComparisonType> : GetOperatorDefinition<O, P>['type'] extends 'array' ? Array<GetFieldTSType<F[I]>> : Extract<GetConditionTSType<GetOperatorDefinition<O, P>['type'] & ConditionOperatorComparisonType>, GetFieldTSType<F[I]>>;
59
72
  validated: true;
60
73
  }
61
74
  export interface UncheckedCondition<O extends OperatorRecord = OperatorRecord, P extends GetConditionOperators<O> = GetConditionOperators<O>> {
62
75
  type: 'condition';
63
76
  operation: P;
64
77
  field: string;
65
- value: GetOperatorDefinition<O, P>['type'] extends 'array' ? Primitive[] : GetConditionTSType<GetOperatorDefinition<O, P>['type'] & ConditionOperatorComparisonType>;
78
+ value: GetUndeclaredConditionTSType<GetOperatorDefinition<O, P>['type'] & ConditionOperatorComparisonType>;
66
79
  validated: false;
67
80
  }
68
81
  export interface Group<F extends FieldTypeRecord = FieldTypeRecord, O extends OperatorRecord = OperatorRecord, V extends boolean = false> {
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "homepage": "https://github.com/exoRift/wizard-ql",
8
8
  "description": "WizardQL is a natural-language-like query language for constructing data queries for resources that meet conditions.",
9
- "version": "3.0.0",
9
+ "version": "4.0.1",
10
10
  "repository": {
11
11
  "type": "git",
12
12
  "url": "git+https://github.com/exoRift/wizard-ql.git"
@@ -45,9 +45,6 @@
45
45
  "eslint-plugin-jsdoc": "^50.6.11",
46
46
  "tsd": "^0.33.0"
47
47
  },
48
- "optionalDependencies": {
49
- "knex": "^3.1.0"
50
- },
51
48
  "peerDependencies": {
52
49
  "typescript": "^5.8.3"
53
50
  }
package/dist/execute.d.ts DELETED
@@ -1,10 +0,0 @@
1
- import type { Knex } from 'knex';
2
- import type { Expression, FieldTypeRecord } from './spec';
3
- import type { WizardParser } from './parser';
4
- /**
5
- * Execute a Wizard expression as a SQL query\
6
- * This mutates the passed query
7
- * @param query The Knex query to append conditions to
8
- * @param expression The Wizard expression
9
- */
10
- export declare function executeAsKnex(query: Knex.QueryBuilder, expression: Expression<FieldTypeRecord, typeof WizardParser.DEFAULT_OPERATORS>): void;