wizard-ql 2.1.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.
package/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2025 Guidance Analytics
189
+ Copyright 2026 Arthur Neuman
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -16,10 +16,12 @@
16
16
  𝗟<sub>anguage</sub>
17
17
  </h2>
18
18
 
19
- #### WizardQL is a natural-language-like query language for constructing data queries for resources that meet conditions.
19
+ #### WizardQL is a natural-language-like query language for constructing data queries for resources that meet conditions, fitted with extensive type completion.
20
+
21
+ [Demo](https://exoRift.github.io/wizard-ql)
20
22
 
21
23
  ## Examples
22
- `parse('(rank >= 10 & role = admin) | banned')`
24
+ `WizardParser.parse('(rank >= 10 & role = admin) | banned')`
23
25
 
24
26
  > ```js
25
27
  > {
@@ -35,27 +37,27 @@
35
37
  > field: "rank",
36
38
  > operation: "GEQ",
37
39
  > value: 10,
38
- > validated: false,
40
+ > validated: false
39
41
  > }, {
40
42
  > type: "condition",
41
43
  > field: "role",
42
44
  > operation: "EQUAL",
43
45
  > value: "admin",
44
- > validated: false,
46
+ > validated: false
45
47
  > }
46
- > ],
48
+ > ]
47
49
  > }, {
48
50
  > type: "condition",
49
51
  > field: "banned",
50
52
  > operation: "EQUAL",
51
53
  > value: true,
52
- > validated: false,
54
+ > validated: false
53
55
  > }
54
- > ],
56
+ > ]
55
57
  > }
56
58
  > ```
57
59
 
58
- `parse('flagged OR (name MATCHES "decorative_.*" AND !(price >= 20 AND price <= 30))')`
60
+ `WizardParser.parse('flagged OR (name MATCHES "decorative_.*" AND !(price >= 20 AND price <= 30))')`
59
61
 
60
62
  > ```js
61
63
  > {
@@ -67,7 +69,7 @@
67
69
  > field: "flagged",
68
70
  > operation: "EQUAL",
69
71
  > value: true,
70
- > validated: false,
72
+ > validated: false
71
73
  > }, {
72
74
  > type: "group",
73
75
  > operation: "AND",
@@ -77,7 +79,7 @@
77
79
  > field: "name",
78
80
  > operation: "MATCH",
79
81
  > value: "decorative_.*",
80
- > validated: false,
82
+ > validated: false
81
83
  > }, {
82
84
  > type: "group",
83
85
  > operation: "OR",
@@ -87,19 +89,19 @@
87
89
  > field: "price",
88
90
  > operation: "LESS",
89
91
  > value: 20,
90
- > validated: false,
92
+ > validated: false
91
93
  > }, {
92
94
  > type: "condition",
93
95
  > field: "price",
94
96
  > operation: "GREATER",
95
97
  > value: 30,
96
- > validated: false,
98
+ > validated: false
97
99
  > }
98
- > ],
100
+ > ]
99
101
  > }
100
- > ],
102
+ > ]
101
103
  > }
102
- > ],
104
+ > ]
103
105
  > }
104
106
  > ```
105
107
 
@@ -128,10 +130,13 @@ Simply denoting a field name (`field`) transforms it into `field = true`
128
130
 
129
131
  (`!field`) transforms it into `field = false`
130
132
 
133
+ > [!NOTE]
134
+ > This is true for the default operator set. If custom operators are defined, a custom [implicit condition](#implicit-condition) can be defined as well.
135
+
131
136
  ### Group
132
137
  *A group is multiple conditions joined by a junction operator such as AND or OR.*
133
138
  `({CONDITION} [...{J_OPERATOR} {CONDITION}])`
134
- Groups are implicit when junction operators are used (follows PEMDAS [ANDs grouped before ORs]). Parentheses can be used to denote them explicitly.
139
+ Groups are implicit when junction operators are used (follows PEMDAS \[ANDs grouped before ORs\]). Parentheses can be used to denote them explicitly.
135
140
 
136
141
  Groups can be nested.
137
142
  > Example: `user.activated & ((user.role = member & user.group : [abc, xyz]) | (user.role = admin & user.privileged))`
@@ -139,7 +144,7 @@ Groups can be nested.
139
144
  Groups can also be negated
140
145
  > `!(firstname = John & lastname = Doe)` &rarr; `firstname != John | lastname != Doe`
141
146
 
142
- ## Operators
147
+ ## Default Operators
143
148
  ### Junction Operators
144
149
  <details>
145
150
  <summary>AND</summary>
@@ -159,7 +164,7 @@ Groups can also be negated
159
164
  - `V`
160
165
  </details>
161
166
 
162
- ### Comparison Operators
167
+ ### Condition Operators
163
168
  <details>
164
169
  <summary>EQUAL</summary>
165
170
 
@@ -243,10 +248,61 @@ Groups can also be negated
243
248
  - `!~`
244
249
  </details>
245
250
 
251
+ ## Custom Operators
252
+ If the default operators don't fulfill your needs, a custom operator set can be defined.
253
+
254
+ ```js
255
+ new WizardParser({
256
+ operators: {
257
+ [OPERATOR_NAME]: {
258
+ negationName: NEGATION_OPERATOR_NAME,
259
+ aliases: ['ALIAS'],
260
+ type: 'numeric',
261
+ negationAliases: ['NEGALIAS']
262
+ },
263
+ ...
264
+ }
265
+ })
266
+ ```
267
+
268
+ An operator is inherently two operators: the base operator and its negation. When defining an operator, you must also define its `negationName`. You can also define aliases for operators and their negations.
269
+
270
+ Lastly, you must supply the data type the operator functions on. This type also determines the operator type (condition/junction \[a junction joins conditions together into a group\]).
271
+ - For **condition operators**, `type` can be `'primitive'`, `'boolean'`, `'string'`, `'number'`, `'date'`, `'numeric'`, or `'array'`. `primitive` allows all data types (singular). `array` allows all data types within an array. `numeric` allows `number` or `date`; however, `number` will be used unless the condition's field is only of type `date`.
272
+ - For **junction operators**, `type` can be `sumjunction` or `productjunction`. They're both group operators, but have different precedence for logical PEMDAS. For example, `A & B | C` becomes `(A & B) | C`. In logic, "and" is a product junction, and "or" is a sum junction. The negation of a sum junction is a product junction and vice-versa.
273
+
274
+ When defining custom operators, their names should be all uppercase. Two operators should not share names or aliases. This applies to the keys of the operator record, `negationName`, `aliases`, and `negationAliases`.
275
+
276
+ For condition operators, if, for whatever reason, you want to define the negation operators first, you can supply `exclusionary: true` which marks the operator you defined in the record as the negatory one.
277
+
278
+ ## Implicit Condition
279
+ You may have noted the existed of [implicit boolean](#implicit-boolean) for the default operator set. Once you start defining custom operators, there is no longer a default implicit condition. One can optionally be defined.
280
+
281
+ ```js
282
+ new WizardParser({
283
+ operators: {
284
+ [OPERATOR_NAME]: {
285
+ negationName: NEGATION_OPERATOR_NAME,
286
+ aliases: ['ALIAS'],
287
+ type: 'numeric',
288
+ negationAliases: ['NEGALIAS']
289
+ },
290
+ ...
291
+ },
292
+ implicitCondition: {
293
+ operator: OPERATOR_NAME, // The operator to use
294
+ value: '0', // The value, as a string
295
+ asType: 'number' // What datatype should the value be read as
296
+ }
297
+ })
298
+ ```
299
+
300
+ The implicit condition is applied when a field name is denoted without any operator. Example: (`field = value | otherfield`). In this case, `otherfield` would be parsed as `otherfield OPERATOR_NAME 0`. If a negator precedes the field (`!otherfield`), it uses the negated variant of the implicit operator.
301
+
246
302
  ## Constraints
247
- The `parse` function can be passed an object containing various constraints as its second parameter
303
+ When constructing a WizardParser instance (`new WizardParser()`), the constructor can be passed properties containing various constraints.
248
304
 
249
- ### `restricted`
305
+ ### Restricted Fields (`restricted`)
250
306
  A record mapping field names to restrictions. A value of `true` totally prohibits the usage of a field.
251
307
 
252
308
  Otherwise, a tuple can be passed
@@ -257,17 +313,17 @@ Values can be direct values (string, number, boolean) or regex expressions
257
313
  "allow" will allow the values/patterns and deny all others
258
314
  "deny" will deny the values/patterns and allow all others
259
315
 
260
- ### `types`
316
+ ### Field Types (`types`)
261
317
  A record mapping field names to (`boolean`, `string`, `number`, `date`). The value in the record can either be a single allowed type or an array of allowed types. Only operators that can function on that type can be used for that field. By default, fields will be treated as being able to be any of the three types.
262
318
 
263
319
  > Example:
264
320
  > ```js
265
- > parse('field1 = value', {
321
+ > new WizardParser({
266
322
  > types: {
267
323
  > field1: 'string',
268
324
  > field2: ['string', 'number']
269
325
  > }
270
- > })
326
+ > }).parse('field1 = value')
271
327
  > ```
272
328
 
273
329
  > [!NOTE]
@@ -284,11 +340,11 @@ When a field has matched either a key in `types` or a field in `restricted`, the
284
340
 
285
341
  Therefore, type inference would look something like this:
286
342
  ```js
287
- const parsed = parse('field = value', {
343
+ const parsed = new WizardParser({
288
344
  types: {
289
345
  field: ['string', 'number']
290
346
  }
291
- })
347
+ }).parse('field = value')
292
348
 
293
349
  if (parsed.validated) {
294
350
  switch (parsed.type) {
@@ -304,6 +360,9 @@ if (parsed.validated) {
304
360
  }
305
361
  ```
306
362
 
363
+ ### `disallowUnvalidated`
364
+ Fields that are not present in the type or restriction record will considered invalid fields. This will make resulting parsed expression types far more usable without having to check if `validated === true`
365
+
307
366
  ### `caseInsensitive`
308
367
  Type/constraint checks will be case-insensitive on the field name
309
368
  > [!NOTE]
@@ -312,41 +371,78 @@ Type/constraint checks will be case-insensitive on the field name
312
371
  > [!WARNING]
313
372
  > Mismatching casing between the restricted record and the type record will prioritize the restricted record
314
373
 
315
- ### `disallowUnvalidated`
316
- Fields that are not present in the type or restriction record will considered invalid fields
317
-
318
374
  ### `dateInterpreter`
319
375
  A callback that determines how WizardQL interprets dates. Wizard will attempt to parse a value as a date if `'date'` is supplied in its type record
320
376
 
321
377
  By default, this is simply `(v) => new Date(v)`
322
378
 
323
379
  ## Stringification
324
- Parsed expressions can be converted back into strings using the `stringify` function. The stringify function comes with its own slew of options as its second parameter
380
+ Parsed expressions can be converted back into strings using the `stringify` method. The stringify method comes with its own options as its second parameter (or it can just be a string, selecting the dialect)
381
+
382
+ ```js
383
+ const parser = new WizardParser()
384
+
385
+ const parsed = parser.parse('...')
386
+ if (parsed) console.log(parser.stringify(parsed, DIALECT))
387
+ ```
388
+
389
+ ### `dialect`
390
+ The dialect determines how operators are stringified.
325
391
 
326
- ### `junctionNotation`
327
- The notation to use for junction operators
392
+ Custom dialects can be supplied in the WizardParser constructor. If using the default operators, the default dialects are:
328
393
  - Programmatic: `&`
329
394
  - Linguistic: `AND`
330
395
  - Formal: `^`
331
396
 
332
- ### `comparisonNotation`
333
- The notation to use for comparison operators
334
- - Programmatic: `=`
335
- - Linguistic: `EQUALS`
336
-
337
397
  ### `alwaysParenthesize`
338
398
  Always put parentheses around every group
339
399
 
340
400
  ### `compact`
341
- Don't include spaces in the output (except for surrounding lingustic operators)
401
+ Don't include spaces in the output. Operators that contain alpha characters will always be surrounded with spaces.
342
402
 
343
- ### `condenseBoolean`
344
- `EQUAL`/`NOTEQUAL` regarding booleans will be condensed into [implicit form](#implicit-boolean)
403
+ ### `condenseImplicit`
404
+ Conditions that are an instance of the [implicit condition](#implicit-condition) or its negation will be condensed into the implicit notation (`field`, `!field`).
405
+
406
+ For example, if the default condition is `some_field LESS 0` (assuming the LESS operator is defined), `field < 0` becomes `field` and `field >= 0` becomes `!field`. There is a special exception for booleans (also utilized for the default operator [implicit boolean](#implicit-boolean)) where `field = false`, which is technically not a negation of the implicit condition (`field != true`), is still condensed to `!field`
407
+
408
+ ### Date Serialization
409
+ A custom callback can be supplied to the parser constructor for how dates should be stringified. The default simply calls `Date.toISOString()`
410
+
411
+ ```js
412
+ new WizardParser({
413
+ dateSerializer: (d) => d.getTime().toString() // Serialize to time in milliseconds instead
414
+ })
415
+ ```
416
+
417
+ ### Custom Dialects
418
+ > See the default dialects [here](#dialect)
419
+
420
+ Whether using the default operator set or custom operators (must define custom dialects for custom operators if you're looking to stringify), custom dialects can be defined. This determines how `WizardParser.stringify()` prints operator names to the resulting string.
421
+
422
+ ```js
423
+ new WizardParser({
424
+ operators: {
425
+ [OPERATOR_NAME]: {
426
+ negationName: NEGATION_OPERATOR_NAME,
427
+ aliases: ['ALIAS'],
428
+ type: 'numeric',
429
+ negationAliases: ['NEGALIAS']
430
+ },
431
+ ...
432
+ },
433
+ dialects: {
434
+ [DIALECT_NAME]: {
435
+ [OPERATOR_NAME]: 'operator_denotation',
436
+ ...
437
+ }
438
+ }
439
+ })
440
+ ```
345
441
 
346
442
  ## Summarize
347
- You can use the `summarize` function to summarize a parsed expression, aggregated by field name across groups
443
+ You can use the `summarize` method to summarize a parsed expression, aggregated by field name across groups. This is useful for authorization checks
348
444
 
349
- `summarize(parse('(foo in [1, 2] and (bar = 2 or baz)) V (bar !: [1, 3] and foo = 3)'))`
445
+ `WizardParser.summarize(parse('(foo in [1, 2] and (bar = 2 or baz)) V (bar !: [1, 3] and foo = 3)'))`
350
446
  > ```js
351
447
  > [
352
448
  > ['foo', [
@@ -384,15 +480,15 @@ You can use the `summarize` function to summarize a parsed expression, aggregate
384
480
  > ```
385
481
 
386
482
  > [!NOTE]
387
- > `exclusionary` implies a negative operation (once that excludes the value)
483
+ > `exclusionary` implies a negatory operation (one that excludes the value)
388
484
 
389
485
  ## Execution Example
390
- Below is an example of how a Wizard query would be executed in the context of a [KnexJS Query](https://knexjs.org/)
486
+ Below is an example of how a Wizard query would be safely executed in the context of a [KnexJS Query](https://knexjs.org)
391
487
 
392
- https://github.com/guidance-analytics/wizard-ql/blob/bf8693e2cb5678ef855600d70bc495a614aa3d0b/src/execute.ts#L1-L35
488
+ https://github.com/exoRift/wizard-ql/blob/424c92e8fc205cf22a639887ebbafd822a7c8731/src/execute.ts#L1-L36
393
489
 
394
490
  ## DOM Input
395
- Wizard comes pre-packaged with a DOM input that applies classes for tokens, making for query input with syntax highlighting (up to discretion)
491
+ Wizard comes pre-packaged with a DOM input that applies classes for tokens, making for a query input with syntax highlighting (up to discretion)
396
492
 
397
493
  ```js
398
494
  // NOTE: The input element should be a regular div element, not an input element
@@ -408,14 +504,16 @@ Depending on a token's type, attributes will be applied to the contents of the i
408
504
  - `data-node` - An actual token
409
505
  - `data-quoted` - Quoted text
410
506
  - `data-number` - A number
411
- - `data-bracket` - A parenthesis or array bracket
412
- - `data-delimiter` - A comma
413
- - `data-negation` - A negatory exclamation mark
414
- - `data-operation` - A comparison or junction operator
507
+ - `data-bracket` - A parenthesis or array bracket (value is the bracket)
508
+ - `data-delimiter` - An array delimiter (comma)
509
+ - `data-negator` - A negation operator (exclamation mark)
510
+ - `data-operator` - A condition or junction operator (value will be 'junction' or 'condition')
415
511
 
416
512
  A token can also possess `data-error` if it is part of an error span
417
513
 
418
514
  The input itself can have the following attributes:
419
515
  - `data-error-message` - The error message
420
516
  - `data-error-start` - The starting token index for the error
421
- - `data-error-end` - The end token index for the error
517
+ - `data-error-end` - The end token index for the error
518
+
519
+ An example for styling this input can be found [here](./test/preview/index.css)
@@ -1,10 +1,10 @@
1
- import { type Token, type TypeRecord } from './spec';
2
- import { type ExpressionConstraints, parse } from './parse';
1
+ import { WizardParser, type WizardParserConfig } from './parser';
3
2
  import { ConstraintError, ParseError } from './errors';
4
- interface DOMInputOptions<T extends TypeRecord, V extends boolean> {
3
+ import type { ClassifiedToken, FieldTypeRecord, OperatorRecord } from './spec';
4
+ interface DOMInputOptions<F extends FieldTypeRecord, O extends OperatorRecord, V extends boolean, D extends string> {
5
5
  input: HTMLElement;
6
- constraints?: ExpressionConstraints<T, V>;
7
- onUpdate?: (expression: ReturnType<typeof parse<T, V>> | ParseError | ConstraintError, tokens: Token[], string: string) => void;
6
+ config?: WizardParserConfig<F, O, V, D>;
7
+ onUpdate?: (expression: ReturnType<WizardParser<F, O, V, D>['parse']> | ParseError | ConstraintError, tokens: ClassifiedToken[], string: string) => void;
8
8
  parseOnInitialize?: boolean;
9
9
  }
10
10
  /**
@@ -12,5 +12,5 @@ interface DOMInputOptions<T extends TypeRecord, V extends boolean> {
12
12
  * @warn This is a DOM function that is not meant for the backend
13
13
  * @returns A destroy function (destroys listening and functionality, not the element)
14
14
  */
15
- export declare function createDOMInput<const T extends TypeRecord, const V extends boolean>({ input, constraints, onUpdate, parseOnInitialize }: DOMInputOptions<T, V>): () => void;
15
+ export declare function createDOMInput<const F extends FieldTypeRecord, const O extends OperatorRecord, const V extends boolean, const D extends string>({ input, config, onUpdate, parseOnInitialize }: DOMInputOptions<F, O, V, D>): () => void;
16
16
  export {};
package/dist/errors.d.ts CHANGED
@@ -15,7 +15,7 @@ export declare class ParseError extends Error {
15
15
  /** The end token index */
16
16
  readonly endIndex: number | undefined;
17
17
  /** The tokens being parsed */
18
- readonly tokens: Token[] | undefined;
18
+ readonly tokens: readonly Token[] | undefined;
19
19
  /**
20
20
  * Construct a Parsing Error
21
21
  * @param message The error message
@@ -25,7 +25,7 @@ export declare class ParseError extends Error {
25
25
  * @param endToken The last token this error pertains to
26
26
  * @param endIndex The end token index
27
27
  */
28
- constructor(message: string, tokens: Token[] | undefined, startToken: Token | undefined, startIndex: number | undefined, endToken?: Token | undefined, endIndex?: number | undefined);
28
+ constructor(message: string, tokens: readonly Token[] | undefined, startToken: Token | undefined, startIndex: number | undefined, endToken?: Token | undefined, endIndex?: number | undefined);
29
29
  }
30
30
  /**
31
31
  * A WizardQL string expression parsing constraint error
@@ -43,7 +43,7 @@ export declare class ConstraintError extends Error {
43
43
  /** The end token index */
44
44
  readonly endIndex: number | undefined;
45
45
  /** The tokens being parsed */
46
- readonly tokens: Token[] | undefined;
46
+ readonly tokens: readonly Token[] | undefined;
47
47
  /**
48
48
  * Construct a Constraint Error
49
49
  * @param message The error message
@@ -53,5 +53,5 @@ export declare class ConstraintError extends Error {
53
53
  * @param endToken The last token this error pertains to
54
54
  * @param endIndex The end token index
55
55
  */
56
- constructor(message: string, tokens: Token[] | undefined, startToken: Token | undefined, startIndex: number | undefined, endToken?: Token | undefined, endIndex?: number | undefined);
56
+ constructor(message: string, tokens: readonly Token[] | undefined, startToken: Token | undefined, startIndex: number | undefined, endToken?: Token | undefined, endIndex?: number | undefined);
57
57
  }
package/dist/execute.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import type { Knex } from 'knex';
2
- import type { Expression } from './spec';
2
+ import type { Expression, FieldTypeRecord } from './spec';
3
+ import type { WizardParser } from './parser';
3
4
  /**
4
5
  * Execute a Wizard expression as a SQL query\
5
6
  * This mutates the passed query
6
7
  * @param query The Knex query to append conditions to
7
8
  * @param expression The Wizard expression
8
9
  */
9
- export declare function executeAsKnex(query: Knex.QueryBuilder, expression: Expression): void;
10
+ export declare function executeAsKnex(query: Knex.QueryBuilder, expression: Expression<FieldTypeRecord, typeof WizardParser.DEFAULT_OPERATORS>): void;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,5 @@
1
- export { type Operation, type ComparisonOperation, type JunctionOperation, type Expression, type UncheckedExpression, type Condition, type UncheckedCondition, type Group, type Primitive, type TypeRecord, type Token, type ComparisonTypeToTSType, OPERATION_PURPOSE_DICTIONARY, COMPARISON_TYPE_DICTIONARY, OPERATION_ALIAS_DICTIONARY } from './spec';
2
- export { type ExpressionConstraints, QUOTE_EDGE_REGEX, parse, tokenize } from './parse';
1
+ export * from './spec';
2
+ export * from './parser';
3
3
  export { executeAsKnex } from './execute';
4
- export { type AggregationValue, summarize } from './summarize';
5
- export { type StringifyOptions, stringify } from './stringify';
6
4
  export { ParseError, ConstraintError } from './errors';
7
5
  export { createDOMInput } from './dominput';