@witchcraft/expressit 0.2.1 → 0.3.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/README.md +6 -4
- package/dist/Lexer.d.ts +102 -99
- package/dist/Lexer.d.ts.map +1 -1
- package/dist/Lexer.js +211 -557
- package/dist/Parser.d.ts +27 -27
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +5 -2
- package/dist/ast/builders/condition.d.ts +1 -1
- package/dist/ast/builders/condition.d.ts.map +1 -1
- package/dist/ast/builders/condition.js +1 -0
- package/dist/ast/builders/delim.d.ts +3 -3
- package/dist/ast/builders/delim.d.ts.map +1 -1
- package/dist/ast/builders/error.d.ts +2 -2
- package/dist/ast/builders/error.d.ts.map +1 -1
- package/dist/ast/builders/expression.d.ts +2 -2
- package/dist/ast/builders/expression.d.ts.map +1 -1
- package/dist/ast/builders/expression.js +2 -6
- package/dist/ast/builders/group.d.ts +1 -1
- package/dist/ast/builders/group.d.ts.map +1 -1
- package/dist/ast/builders/group.js +1 -3
- package/dist/ast/builders/pos.d.ts +2 -2
- package/dist/ast/builders/pos.d.ts.map +1 -1
- package/dist/ast/builders/token.d.ts +2 -2
- package/dist/ast/builders/token.d.ts.map +1 -1
- package/dist/ast/builders/type.d.ts +2 -2
- package/dist/ast/builders/type.d.ts.map +1 -1
- package/dist/ast/builders/variable.d.ts +3 -3
- package/dist/ast/builders/variable.d.ts.map +1 -1
- package/dist/ast/createConditionNode.d.ts +1 -1
- package/dist/ast/createConditionNode.d.ts.map +1 -1
- package/dist/ast/createGroupNode.d.ts +1 -1
- package/dist/ast/createGroupNode.d.ts.map +1 -1
- package/dist/ast/createToken.d.ts +2 -2
- package/dist/ast/createToken.d.ts.map +1 -1
- package/dist/ast/createToken.js +2 -2
- package/dist/ast/error.d.ts +2 -2
- package/dist/ast/error.d.ts.map +1 -1
- package/dist/ast/error.js +1 -0
- package/dist/ast/handlers.d.ts +23 -23
- package/dist/ast/handlers.d.ts.map +1 -1
- package/dist/ast/handlers.js +4 -4
- package/dist/examples/ParserWithSqlSupport.d.ts +62 -0
- package/dist/examples/ParserWithSqlSupport.d.ts.map +1 -0
- package/dist/examples/ParserWithSqlSupport.js +271 -0
- package/dist/examples/{shortcutContextParser.d.ts → ShortcutContextParser.d.ts} +5 -5
- package/dist/examples/ShortcutContextParser.d.ts.map +1 -0
- package/dist/examples/{shortcutContextParser.js → ShortcutContextParser.js} +2 -2
- package/dist/examples/index.d.ts +2 -1
- package/dist/examples/index.d.ts.map +1 -1
- package/dist/examples/index.js +3 -1
- package/dist/index.js +2 -2
- package/dist/internal/ExpressitError.d.ts +2 -2
- package/dist/internal/ExpressitError.d.ts.map +1 -1
- package/dist/internal/ExpressitError.js +2 -1
- package/dist/internal/checkParserOpts.d.ts +1 -1
- package/dist/internal/checkParserOpts.d.ts.map +1 -1
- package/dist/internal/checkParserOpts.js +11 -11
- package/dist/internal/parseParserOptions.d.ts +1 -1
- package/dist/internal/parseParserOptions.d.ts.map +1 -1
- package/dist/package.json.js +4 -195
- package/dist/types/ast.d.ts +60 -58
- package/dist/types/ast.d.ts.map +1 -1
- package/dist/types/ast.js +26 -27
- package/dist/types/autocomplete.d.ts +23 -21
- package/dist/types/autocomplete.d.ts.map +1 -1
- package/dist/types/autocomplete.js +24 -21
- package/dist/types/errors.d.ts +12 -10
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +8 -7
- package/dist/types/index.js +2 -2
- package/dist/types/parser.d.ts +9 -9
- package/dist/types/parser.d.ts.map +1 -1
- package/dist/utils/getCursorInfo.js +3 -1
- package/dist/utils/getOppositeDelimiter.d.ts +2 -2
- package/dist/utils/getOppositeDelimiter.d.ts.map +1 -1
- package/dist/utils/isDelimiter.d.ts +2 -2
- package/dist/utils/isDelimiter.d.ts.map +1 -1
- package/dist/utils/isParen.d.ts +2 -2
- package/dist/utils/isParen.d.ts.map +1 -1
- package/dist/utils/isQuote.d.ts +2 -2
- package/dist/utils/isQuote.d.ts.map +1 -1
- package/package.json +29 -27
- package/src/Lexer.ts +103 -92
- package/src/Parser.ts +70 -64
- package/src/ast/builders/condition.ts +3 -3
- package/src/ast/builders/delim.ts +5 -5
- package/src/ast/builders/error.ts +3 -3
- package/src/ast/builders/expression.ts +4 -8
- package/src/ast/builders/group.ts +2 -4
- package/src/ast/builders/pos.ts +3 -3
- package/src/ast/builders/token.ts +2 -2
- package/src/ast/builders/type.ts +2 -2
- package/src/ast/builders/variable.ts +5 -5
- package/src/ast/createConditionNode.ts +2 -2
- package/src/ast/createGroupNode.ts +4 -4
- package/src/ast/createToken.ts +6 -6
- package/src/ast/error.ts +2 -2
- package/src/ast/handlers.ts +23 -23
- package/src/examples/ParserWithSqlSupport.ts +371 -0
- package/src/examples/{shortcutContextParser.ts → ShortcutContextParser.ts} +14 -14
- package/src/examples/index.ts +2 -1
- package/src/internal/ExpressitError.ts +4 -4
- package/src/internal/checkParserOpts.ts +14 -14
- package/src/internal/parseParserOptions.ts +2 -2
- package/src/types/ast.ts +101 -96
- package/src/types/autocomplete.ts +26 -22
- package/src/types/errors.ts +18 -13
- package/src/types/parser.ts +9 -9
- package/src/utils/getCursorInfo.ts +1 -1
- package/src/utils/getOppositeDelimiter.ts +2 -2
- package/src/utils/getSurroundingErrors.ts +4 -4
- package/src/utils/isDelimiter.ts +3 -3
- package/src/utils/isParen.ts +2 -2
- package/src/utils/isQuote.ts +2 -2
- package/dist/examples/shortcutContextParser.d.ts.map +0 -1
- package/dist/global.d.js +0 -1
- package/dist/package.js +0 -7
- package/src/global.d.ts +0 -4
- package/src/package.js +0 -11
|
@@ -9,15 +9,15 @@ export function createGroupNode<
|
|
|
9
9
|
TPrefix extends
|
|
10
10
|
TPrefixable extends true
|
|
11
11
|
? ConditionNode<TValid> |
|
|
12
|
-
ValidToken<TOKEN_TYPE.NOT> |
|
|
12
|
+
ValidToken<typeof TOKEN_TYPE.NOT> |
|
|
13
13
|
undefined
|
|
14
|
-
: ValidToken<TOKEN_TYPE.NOT> |
|
|
14
|
+
: ValidToken<typeof TOKEN_TYPE.NOT> |
|
|
15
15
|
undefined =
|
|
16
16
|
TPrefixable extends true
|
|
17
17
|
? ConditionNode<TValid> |
|
|
18
|
-
ValidToken<TOKEN_TYPE.NOT> |
|
|
18
|
+
ValidToken<typeof TOKEN_TYPE.NOT> |
|
|
19
19
|
undefined
|
|
20
|
-
: ValidToken<TOKEN_TYPE.NOT>,
|
|
20
|
+
: ValidToken<typeof TOKEN_TYPE.NOT>,
|
|
21
21
|
>(raw: {
|
|
22
22
|
prefix: TPrefix
|
|
23
23
|
} & RawNode<GroupNode<TValid>>): GroupNode<TValid, TPrefixable, TPrefix> {
|
package/src/ast/createToken.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { ExpressitError } from "../internal/ExpressitError.js"
|
|
2
|
-
import type { ErrorToken, RawToken,
|
|
3
|
-
import {
|
|
2
|
+
import type { ErrorToken, RawToken, TokenType, ValidToken } from "../types/ast.js"
|
|
3
|
+
import { PARSER_ERROR } from "../types/errors.js"
|
|
4
4
|
|
|
5
5
|
export function createToken<
|
|
6
|
-
TType extends
|
|
6
|
+
TType extends TokenType,
|
|
7
7
|
>(raw: { type: TType } & RawToken<ValidToken>): ValidToken<TType>
|
|
8
8
|
export function createToken(raw: RawToken<ErrorToken>): ErrorToken
|
|
9
9
|
export function createToken<
|
|
10
10
|
TValid extends boolean = boolean,
|
|
11
11
|
TType extends
|
|
12
|
-
TValid extends true ?
|
|
13
|
-
TValid extends true ?
|
|
12
|
+
TValid extends true ? TokenType : never =
|
|
13
|
+
TValid extends true ? TokenType : never
|
|
14
14
|
>(raw: RawToken<ValidToken> | RawToken<ErrorToken>): TValid extends true
|
|
15
15
|
? ValidToken<TType>
|
|
16
16
|
: ErrorToken {
|
|
17
17
|
if (raw.start === undefined || raw.end === undefined) {
|
|
18
|
-
throw new ExpressitError(
|
|
18
|
+
throw new ExpressitError(PARSER_ERROR.POSITION_ERROR, raw)
|
|
19
19
|
}
|
|
20
20
|
return {
|
|
21
21
|
...raw,
|
package/src/ast/error.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createToken } from "./createToken.js"
|
|
2
2
|
|
|
3
|
-
import { type ErrorToken,type
|
|
3
|
+
import { type ErrorToken,type TokenType } from "../types/ast.js"
|
|
4
4
|
|
|
5
|
-
export function error<T extends
|
|
5
|
+
export function error<T extends TokenType>(pos: number, expected: T[]): ErrorToken {
|
|
6
6
|
if (pos === undefined) throw new Error("should never happen, passed undefined position for error token")
|
|
7
7
|
return createToken({ expected, start: pos, end: pos })
|
|
8
8
|
}
|
package/src/ast/handlers.ts
CHANGED
|
@@ -7,28 +7,28 @@ import { createGroupNode } from "./createGroupNode.js"
|
|
|
7
7
|
import { createToken } from "./createToken.js"
|
|
8
8
|
import { createVariableNode } from "./createVariableNode.js"
|
|
9
9
|
|
|
10
|
-
import { type AnyToken, type ArrayNode,AST_TYPE,type ConditionNode, type ErrorToken, type ExpressionNode,type FirstParam,type GroupNode, type Position, TOKEN_TYPE, type
|
|
10
|
+
import { type AnyToken, type ArrayNode,AST_TYPE,type ConditionNode, type ErrorToken, type ExpressionNode,type FirstParam,type GroupNode, type Position, TOKEN_TYPE, type TokenBoolean, type TokenDelimiter, type TokenOperator, type TokenQuote, type TokenType, type ValidToken, type VariableNode } from "../types/ast.js"
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
/* #region HELPERS */
|
|
14
|
-
function error<T extends
|
|
14
|
+
function error<T extends TokenType>(pos: number, expected: T[]): ErrorToken {
|
|
15
15
|
if (pos === undefined) throw new Error("should never happen, passed undefined position for error token")
|
|
16
16
|
return createToken({ expected, start: pos, end: pos })
|
|
17
17
|
}
|
|
18
18
|
/* #regionend */
|
|
19
19
|
|
|
20
20
|
/* #region TOKENS */
|
|
21
|
-
const operators = <T extends
|
|
21
|
+
const operators = <T extends TokenOperator>
|
|
22
22
|
(type: T) =>
|
|
23
23
|
(value: string, pos: Position): ValidToken<T> => createToken({ value, type, ...pos })
|
|
24
24
|
|
|
25
|
-
const delimiters = <T extends
|
|
25
|
+
const delimiters = <T extends TokenDelimiter>
|
|
26
26
|
(type: T) =>
|
|
27
27
|
(value: string | undefined, pos: Position): ValidToken<T> | undefined =>
|
|
28
28
|
// check must be falsy, we want to return undefined when given ""
|
|
29
29
|
value ? createToken({ value, type, ...pos }) : undefined
|
|
30
30
|
|
|
31
|
-
const maybeToken = <T extends
|
|
31
|
+
const maybeToken = <T extends TokenType> (type: T) => <TVal extends string | undefined> (value: TVal, pos: Position): TVal extends string ? ValidToken<T> : ErrorToken => {
|
|
32
32
|
if (value === undefined) {
|
|
33
33
|
return error(pos.end, [type]) as any
|
|
34
34
|
} else {
|
|
@@ -63,11 +63,11 @@ export const operator = {
|
|
|
63
63
|
|
|
64
64
|
/* #region AST NODES */
|
|
65
65
|
export function variable(
|
|
66
|
-
prefix: ValidToken<TOKEN_TYPE.VALUE> | null | undefined,
|
|
67
|
-
quoteL: AnyToken<
|
|
68
|
-
value: AnyToken<TOKEN_TYPE.VALUE> | null | undefined,
|
|
69
|
-
quoteR: AnyToken<
|
|
70
|
-
flags?: ValidToken<TOKEN_TYPE.VALUE>,
|
|
66
|
+
prefix: ValidToken<typeof TOKEN_TYPE.VALUE> | null | undefined,
|
|
67
|
+
quoteL: AnyToken<TokenQuote> | null | undefined,
|
|
68
|
+
value: AnyToken<typeof TOKEN_TYPE.VALUE> | null | undefined,
|
|
69
|
+
quoteR: AnyToken<TokenQuote> | null | undefined,
|
|
70
|
+
flags?: ValidToken<typeof TOKEN_TYPE.VALUE>,
|
|
71
71
|
): VariableNode {
|
|
72
72
|
const node: FirstParam<typeof createVariableNode> = {
|
|
73
73
|
prefix: prefix ?? undefined,
|
|
@@ -92,19 +92,19 @@ export function variable(
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
export function condition(
|
|
95
|
-
not: ValidToken<TOKEN_TYPE.NOT> | null | undefined,
|
|
95
|
+
not: ValidToken<typeof TOKEN_TYPE.NOT> | null | undefined,
|
|
96
96
|
property: VariableNode | null | undefined,
|
|
97
97
|
{ propertyOperator, sepL, sepR }: {
|
|
98
98
|
propertyOperator?: ConditionNode["propertyOperator"] | null
|
|
99
|
-
sepL?: ValidToken<TOKEN_TYPE.OP_EXPANDED_SEP> | null
|
|
100
|
-
sepR?: ValidToken<TOKEN_TYPE.OP_EXPANDED_SEP> | null
|
|
99
|
+
sepL?: ValidToken<typeof TOKEN_TYPE.OP_EXPANDED_SEP> | null
|
|
100
|
+
sepR?: ValidToken<typeof TOKEN_TYPE.OP_EXPANDED_SEP> | null
|
|
101
101
|
} = {},
|
|
102
102
|
value?: VariableNode | GroupNode | ArrayNode | null,
|
|
103
103
|
): ConditionNode {
|
|
104
104
|
const start = (not?.start ?? property?.start ?? sepL?.start ?? propertyOperator?.start ?? sepR?.start ?? value?.start)!
|
|
105
105
|
const end = (value?.end ?? sepR?.end ?? propertyOperator?.end ?? sepL?.end ?? property?.end ?? not?.end)!
|
|
106
106
|
const node: FirstParam<typeof createConditionNode> = {
|
|
107
|
-
value: value
|
|
107
|
+
value: value ?? error(end, [TOKEN_TYPE.VALUE]),
|
|
108
108
|
start,
|
|
109
109
|
end,
|
|
110
110
|
}
|
|
@@ -115,22 +115,22 @@ export function condition(
|
|
|
115
115
|
node.sep = {}
|
|
116
116
|
if (sepL) {
|
|
117
117
|
node.sep.left = sepL
|
|
118
|
-
node.property
|
|
119
|
-
node.propertyOperator
|
|
118
|
+
node.property ??= error(sepL.start, [TOKEN_TYPE.VALUE])
|
|
119
|
+
node.propertyOperator ??= error(sepL?.end ?? sepR?.start, [TOKEN_TYPE.VALUE])
|
|
120
120
|
}
|
|
121
121
|
if (sepR) node.sep.right = sepR
|
|
122
122
|
else if (!node.value || node.value.type === AST_TYPE.VARIABLE) {
|
|
123
123
|
node.sep.right = error(node.value?.start ?? end, [TOKEN_TYPE.OP_EXPANDED_SEP])
|
|
124
124
|
}
|
|
125
125
|
} else if (propertyOperator) {
|
|
126
|
-
node.property
|
|
126
|
+
node.property ??= error(propertyOperator.start, [TOKEN_TYPE.VALUE])
|
|
127
127
|
}
|
|
128
128
|
return createConditionNode(node as ConditionNode)
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
export function expression(
|
|
132
132
|
left: ConditionNode | GroupNode | null | undefined,
|
|
133
|
-
operator: ValidToken<
|
|
133
|
+
operator: ValidToken<TokenBoolean> | null | undefined,
|
|
134
134
|
right: ConditionNode | GroupNode | null | undefined,
|
|
135
135
|
): ExpressionNode {
|
|
136
136
|
return createExpressionNode({
|
|
@@ -143,11 +143,11 @@ export function expression(
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
export function group(
|
|
146
|
-
operator: ValidToken<TOKEN_TYPE.NOT> | null | undefined,
|
|
146
|
+
operator: ValidToken<typeof TOKEN_TYPE.NOT> | null | undefined,
|
|
147
147
|
prefix: ConditionNode | null | undefined,
|
|
148
|
-
parenL: ValidToken<TOKEN_TYPE.PARENL> | null | undefined,
|
|
148
|
+
parenL: ValidToken<typeof TOKEN_TYPE.PARENL> | null | undefined,
|
|
149
149
|
condition: GroupNode["expression"],
|
|
150
|
-
parenR: ValidToken<TOKEN_TYPE.PARENR> | null | undefined,
|
|
150
|
+
parenR: ValidToken<typeof TOKEN_TYPE.PARENR> | null | undefined,
|
|
151
151
|
): GroupNode {
|
|
152
152
|
return createGroupNode({
|
|
153
153
|
prefix: prefix ?? operator ?? undefined,
|
|
@@ -162,9 +162,9 @@ export function group(
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
export function array(
|
|
165
|
-
bracketL: ValidToken<TOKEN_TYPE.BRACKETL>,
|
|
165
|
+
bracketL: ValidToken<typeof TOKEN_TYPE.BRACKETL>,
|
|
166
166
|
values: VariableNode[],
|
|
167
|
-
bracketR: ValidToken<TOKEN_TYPE.BRACKETR> | null | undefined,
|
|
167
|
+
bracketR: ValidToken<typeof TOKEN_TYPE.BRACKETR> | null | undefined,
|
|
168
168
|
): ArrayNode {
|
|
169
169
|
return createArrayNode({
|
|
170
170
|
values,
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { unreachable } from "@alanscodelog/utils/unreachable.js"
|
|
2
|
+
|
|
3
|
+
import { Parser } from "../Parser.js"
|
|
4
|
+
import { AST_TYPE, type NormalizedCondition, type NormalizedExpression, type Position,TOKEN_TYPE } from "../types/ast.js"
|
|
5
|
+
|
|
6
|
+
export interface BasePropertyDefinition {
|
|
7
|
+
name: string
|
|
8
|
+
/** Supported types are: string, boolean, int, float, date. */
|
|
9
|
+
type: string
|
|
10
|
+
/** Return how to access the column in the WHERE condition of the SQL query, useful for json properties. It is up to you to properly quote the value if needed. */
|
|
11
|
+
transformToColumn?: (key: string, name: string) => string
|
|
12
|
+
/**
|
|
13
|
+
* A function that can be used to transform the value before it is inserted into the SQL query. Useful for types like arrays. It is up to you to properly escape values if you use the second parameter which contains the unescaped value.
|
|
14
|
+
*/
|
|
15
|
+
transformValue?: (value: any, unescapedValue: any) => any | any[]
|
|
16
|
+
isArray?: boolean
|
|
17
|
+
/** If undefined, it's assumed all operators are supported. This should only include the final operator (see {@link BaseOperatorDefinition.operator}), it doesn't need to include it's aliases. */
|
|
18
|
+
supportedOperators?: string[]
|
|
19
|
+
/** Further transform the value after the basic parsing has been done. Useful to, for example, parse other strings (e.g. now, today, tomorrow) as dates. */
|
|
20
|
+
postParse?: (value: any) => any
|
|
21
|
+
}
|
|
22
|
+
export interface BaseOperatorDefinition {
|
|
23
|
+
/** The final operator to use in the SQL query and while evaluating. */
|
|
24
|
+
operator: string
|
|
25
|
+
/** All aliases the user can use to specify the operator. They need not include the real final operator. */
|
|
26
|
+
operators: string[]
|
|
27
|
+
/** All negated aliases to the operator. If an operator is listed here, it will be used to "invert" a condition when normalizing. e.g. < can list >= as a negated operator. This greatly simplifies queries. */
|
|
28
|
+
negatedOperators?: string[]
|
|
29
|
+
/** How to compare the value when evualuating a condition. This is only used if using `evaluate`. */
|
|
30
|
+
valueComparer: (condition: any, contextValue: any) => boolean
|
|
31
|
+
}
|
|
32
|
+
export type SqlParserError =
|
|
33
|
+
| "invalidKey"
|
|
34
|
+
| "unknownProperty"
|
|
35
|
+
| "unknownOperator"
|
|
36
|
+
| "unknownOperatorForType"
|
|
37
|
+
| "invalidValueType"
|
|
38
|
+
/*
|
|
39
|
+
* Creates an example parser with a `toSql` method that can be used to convert an AST to a a safe SQL query.
|
|
40
|
+
*
|
|
41
|
+
* The parser assumes all column names (i.e. the propertyDefinitions) and operators (i.e. operatorDefinitions) are vetted and safe to use in the SQL queries. They are double quoted (unless transformToColumn is specificed) so you can use special characters if needed. You can use the `transformToColumn` option to specify a custom name (e.g. for json columns) that will be left as is.
|
|
42
|
+
*
|
|
43
|
+
* Values are escaped with the given `sqlEscapeValue`. If using drizzle, for simple tables and types, you can just do and it'll be safe. More complex types, such as arrays, or json properties are more comlicated and you'll probably want to specify the definition's transformValue option. See the examples tests for examples.
|
|
44
|
+
* ```ts
|
|
45
|
+
* const parser = new ParserWithSqlSupport(
|
|
46
|
+
* propertyDefinitions,
|
|
47
|
+
* {
|
|
48
|
+
* sqlEscapeValue: (value: string) => sql`${value}`
|
|
49
|
+
* }
|
|
50
|
+
* )
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
*/
|
|
54
|
+
export class ParserWithSqlSupport<TErrorToken extends
|
|
55
|
+
Position & {
|
|
56
|
+
type: SqlParserError
|
|
57
|
+
message?: string
|
|
58
|
+
} =
|
|
59
|
+
Position & {
|
|
60
|
+
type: SqlParserError
|
|
61
|
+
message?: string
|
|
62
|
+
},
|
|
63
|
+
TPropertyDefinition extends BasePropertyDefinition = BasePropertyDefinition,
|
|
64
|
+
TPropertyDefinitions extends Record<string, TPropertyDefinition> = Record<string, TPropertyDefinition>,
|
|
65
|
+
TOperatorDefinition extends BaseOperatorDefinition = BaseOperatorDefinition,
|
|
66
|
+
TOperatorDefinitions extends Record<string, TOperatorDefinition> = Record<string, TOperatorDefinition>,
|
|
67
|
+
TSqlEscapeValue extends (value: string) => any | ReturnType<TSqlEscapeValue> = (value: string) => any,
|
|
68
|
+
> extends Parser<TErrorToken> {
|
|
69
|
+
sqlEscapeValue: TSqlEscapeValue
|
|
70
|
+
|
|
71
|
+
operatorMap: Record<string, string>
|
|
72
|
+
|
|
73
|
+
propertyDefinitions: TPropertyDefinitions
|
|
74
|
+
|
|
75
|
+
operatorDefinitions: TOperatorDefinitions
|
|
76
|
+
|
|
77
|
+
constructor(
|
|
78
|
+
propertyDefinitions: TPropertyDefinitions,
|
|
79
|
+
operatorDefinitions: TOperatorDefinitions,
|
|
80
|
+
{ sqlEscapeValue }: { sqlEscapeValue: TSqlEscapeValue }
|
|
81
|
+
) {
|
|
82
|
+
const operators = []
|
|
83
|
+
const operatorMap: Record<string, string> = {}
|
|
84
|
+
for (const value of Object.values(operatorDefinitions)) {
|
|
85
|
+
for (const operator of value.operators) {
|
|
86
|
+
operatorMap[operator] = value.operator
|
|
87
|
+
operators.push(operator)
|
|
88
|
+
}
|
|
89
|
+
if (value.negatedOperators) {
|
|
90
|
+
for (const operator of value.negatedOperators) {
|
|
91
|
+
operatorMap[operator] = value.operator
|
|
92
|
+
operators.push(operator)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
super({
|
|
97
|
+
arrayValues: true,
|
|
98
|
+
regexValues: false,
|
|
99
|
+
keywords: {
|
|
100
|
+
and: [{ isSymbol: true, value: "&&" }],
|
|
101
|
+
or: [{ isSymbol: true, value: "||" }],
|
|
102
|
+
not: [{ isSymbol: true, value: "!" }],
|
|
103
|
+
},
|
|
104
|
+
customPropertyOperators: operators,
|
|
105
|
+
prefixableGroups: false,
|
|
106
|
+
valueComparer: (condition, contextValue, _context) => {
|
|
107
|
+
if (typeof condition.value !== typeof contextValue) {
|
|
108
|
+
throw new Error(`Expected type of property ${condition.property[0]} to be the same type as the context value ${contextValue} (${typeof contextValue}). If the ast has been validated this is likely because the type of the context value is incorrect.`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const prop = condition.property[0]
|
|
112
|
+
if (!prop) unreachable("Did you validate the ast before evaluating it?")
|
|
113
|
+
|
|
114
|
+
const propDefinition = propertyDefinitions[prop]
|
|
115
|
+
const operatorDefinition = condition.operator && operatorDefinitions[condition.operator]
|
|
116
|
+
if (!operatorDefinition) unreachable("Did you validate the ast before evaluating it?")
|
|
117
|
+
|
|
118
|
+
const isSupported = !propDefinition.supportedOperators?.includes(condition.operator!)
|
|
119
|
+
if (!isSupported) unreachable("Did you validate the ast before evaluating it?")
|
|
120
|
+
|
|
121
|
+
const res = operatorDefinition.valueComparer(condition, contextValue)
|
|
122
|
+
return res
|
|
123
|
+
},
|
|
124
|
+
valueValidator: (_contextValue, query): TErrorToken[] | void => {
|
|
125
|
+
const prop = query.propertyKeys[0]
|
|
126
|
+
let tokens: TErrorToken[] = []
|
|
127
|
+
const propDefinition = propertyDefinitions[prop]
|
|
128
|
+
if (!propDefinition) {
|
|
129
|
+
tokens = tokens.concat(query.property.map(token => ({
|
|
130
|
+
start: token.start,
|
|
131
|
+
end: token.end,
|
|
132
|
+
type: "unknownProperty",
|
|
133
|
+
})) as TErrorToken[])
|
|
134
|
+
return tokens
|
|
135
|
+
}
|
|
136
|
+
const op = query.operator
|
|
137
|
+
const opKey = op && operatorMap[op.value]
|
|
138
|
+
if (!op || !opKey) {
|
|
139
|
+
tokens.push({
|
|
140
|
+
start: (op ?? query.condition)?.start,
|
|
141
|
+
end: (op ?? query.condition)?.end,
|
|
142
|
+
type: "unknownOperator",
|
|
143
|
+
} as TErrorToken)
|
|
144
|
+
} else {
|
|
145
|
+
if (propDefinition.supportedOperators && !propDefinition.supportedOperators?.includes(opKey)) {
|
|
146
|
+
tokens.push({
|
|
147
|
+
start: query.condition.start,
|
|
148
|
+
end: query.condition.end,
|
|
149
|
+
type: "unknownOperatorForType",
|
|
150
|
+
} as TErrorToken)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const val = query.value
|
|
155
|
+
if (Array.isArray(val)) {
|
|
156
|
+
for (const v of val) {
|
|
157
|
+
if (v.type !== "VARIABLE") unreachable()
|
|
158
|
+
const res = convertAndValidateValue(query.isQuoted, v.value.value, prop, propertyDefinitions, { isArray: true })
|
|
159
|
+
if (res instanceof Error) {
|
|
160
|
+
if (v.type !== "VARIABLE") unreachable()
|
|
161
|
+
const token = v
|
|
162
|
+
tokens.push({
|
|
163
|
+
start: token.start,
|
|
164
|
+
end: token.end,
|
|
165
|
+
type: "invalidValueType",
|
|
166
|
+
message: res.message,
|
|
167
|
+
} as TErrorToken)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (tokens.length > 0) return tokens
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (val?.type !== "VARIABLE") unreachable()
|
|
175
|
+
const value = val.value.value
|
|
176
|
+
const res = convertAndValidateValue(query.isQuoted, value, prop, propertyDefinitions)
|
|
177
|
+
if (res instanceof Error) {
|
|
178
|
+
if (!query.value || query.value.type !== "VARIABLE") unreachable()
|
|
179
|
+
const token = query.value.value
|
|
180
|
+
tokens.push({
|
|
181
|
+
start: token.start,
|
|
182
|
+
end: token.end,
|
|
183
|
+
type: "invalidValueType",
|
|
184
|
+
message: res.message,
|
|
185
|
+
} as TErrorToken)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (tokens.length > 0) return tokens
|
|
189
|
+
},
|
|
190
|
+
conditionNormalizer(query) {
|
|
191
|
+
const prop = query.property?.[0]
|
|
192
|
+
if (!prop) unreachable("Did you validate the ast before normalizing it?")
|
|
193
|
+
const propDefinition = propertyDefinitions[prop]
|
|
194
|
+
|
|
195
|
+
let finalValue
|
|
196
|
+
if (Array.isArray(query.value)) {
|
|
197
|
+
const values = []
|
|
198
|
+
if (query.condition.value.type !== AST_TYPE.ARRAY) unreachable()
|
|
199
|
+
const raw = query.condition.value.values
|
|
200
|
+
for (let i = 0; i < query.value.length; i += 1) {
|
|
201
|
+
const token = raw[i]
|
|
202
|
+
const val = query.value[i]
|
|
203
|
+
const isQuoted = !!token.quote
|
|
204
|
+
const res = convertAndValidateValue(isQuoted, val, prop, propertyDefinitions, { isArray: true })
|
|
205
|
+
if (res instanceof Error) throw res
|
|
206
|
+
values.push(res)
|
|
207
|
+
}
|
|
208
|
+
finalValue = values
|
|
209
|
+
} else {
|
|
210
|
+
finalValue = convertAndValidateValue(query.isQuoted, query.value, prop, propertyDefinitions)
|
|
211
|
+
if (propDefinition.isArray) {
|
|
212
|
+
finalValue = [finalValue]
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let finalOperator: any = query.operator
|
|
217
|
+
if (finalValue instanceof Error) throw finalValue
|
|
218
|
+
const opKey = query.operator && operatorMap[query.operator]
|
|
219
|
+
if (!opKey) unreachable("Did you validate the ast before normalizing it?")
|
|
220
|
+
|
|
221
|
+
const operatorDefinition = opKey && operatorDefinitions[opKey]
|
|
222
|
+
if (!operatorDefinition) unreachable("Did you validate the ast before normalizing it?")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
const isNegatableOperator = operatorDefinition.negatedOperators?.includes(query.operator!)
|
|
226
|
+
|
|
227
|
+
finalOperator = operatorDefinition.operator
|
|
228
|
+
let isNegated = query.isNegated
|
|
229
|
+
if (isNegatableOperator) {
|
|
230
|
+
isNegated = !isNegated
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { value: finalValue, operator: finalOperator, negate: isNegated }
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
this.propertyDefinitions = propertyDefinitions
|
|
237
|
+
this.operatorDefinitions = operatorDefinitions
|
|
238
|
+
this.operatorMap = operatorMap
|
|
239
|
+
this.sqlEscapeValue = sqlEscapeValue
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
toSql<T>(
|
|
243
|
+
ast: NormalizedExpression<any, any> | NormalizedCondition<any, any>,
|
|
244
|
+
/**
|
|
245
|
+
* Optionally convert the raw strings to something else. If uding drizzle, you can pass sql.raw here. So later you can just sql.join the return of the function:
|
|
246
|
+
*
|
|
247
|
+
* ```ts
|
|
248
|
+
* sql.join([
|
|
249
|
+
* sql.raw(db.select().from(someTable).toSQL().sql),
|
|
250
|
+
* sql.raw(`where`),
|
|
251
|
+
* ...parser.toSql(ast, sql.raw)
|
|
252
|
+
* ], sql` `)
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
wrapStrings?: (value: string) => T,
|
|
256
|
+
): (ReturnType<TSqlEscapeValue> | typeof wrapStrings extends undefined ? string : T)[] {
|
|
257
|
+
this._checkEvaluationOptions()
|
|
258
|
+
const chunks = []
|
|
259
|
+
|
|
260
|
+
if (ast.type === AST_TYPE.NORMALIZED_CONDITION) {
|
|
261
|
+
const prop = ast.property?.[0]
|
|
262
|
+
const definition = this.propertyDefinitions[prop]
|
|
263
|
+
const value = ast.value
|
|
264
|
+
const col = definition.transformToColumn?.(prop, definition.name) ?? `"${prop}"`
|
|
265
|
+
const op = ast.operator
|
|
266
|
+
if (ast.negate) {
|
|
267
|
+
chunks.push(wrapStrings?.(`NOT(`) ?? `NOT(`)
|
|
268
|
+
}
|
|
269
|
+
chunks.push(wrapStrings?.(`${col} `) ?? `${col} `)
|
|
270
|
+
chunks.push(wrapStrings?.(`${op} `) ?? `${op} `)
|
|
271
|
+
const val = this.sqlEscapeValue(value)
|
|
272
|
+
if (definition.transformValue) {
|
|
273
|
+
const transformed = definition.transformValue(val, value)
|
|
274
|
+
if (Array.isArray(transformed)) {
|
|
275
|
+
chunks.push(...transformed)
|
|
276
|
+
} else {
|
|
277
|
+
chunks.push(transformed)
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
chunks.push(val)
|
|
281
|
+
}
|
|
282
|
+
if (ast.negate) {
|
|
283
|
+
chunks.push(wrapStrings?.(`)`) ?? `)`)
|
|
284
|
+
}
|
|
285
|
+
return chunks
|
|
286
|
+
}
|
|
287
|
+
if (ast.type === AST_TYPE.NORMALIZED_EXPRESSION) {
|
|
288
|
+
const left = this.toSql(ast.left, wrapStrings)
|
|
289
|
+
const right = this.toSql(ast.right, wrapStrings)
|
|
290
|
+
const op = ast.operator === TOKEN_TYPE.AND ? "AND" : "OR"
|
|
291
|
+
chunks.push(wrapStrings?.(`(`) ?? `(`)
|
|
292
|
+
chunks.push(...left)
|
|
293
|
+
chunks.push(wrapStrings?.(` ${op} `) ?? ` ${op} `)
|
|
294
|
+
chunks.push(...right)
|
|
295
|
+
chunks.push(wrapStrings?.(`)`) ?? `)`)
|
|
296
|
+
return chunks as any
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return unreachable()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
export function createTypeError(prop: string, type: string, isArray: boolean): Error {
|
|
303
|
+
if (isArray) {
|
|
304
|
+
return new Error(`Property ${prop} must contain items of type ${type}.`)
|
|
305
|
+
}
|
|
306
|
+
return new Error(`Property ${prop} must be of type ${type}.`)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function convertAndValidateValue(
|
|
310
|
+
isQuoted: boolean,
|
|
311
|
+
value: any,
|
|
312
|
+
prop: string,
|
|
313
|
+
propertyDefinitions: Record<string, BasePropertyDefinition>,
|
|
314
|
+
{ isArray = false }: { isArray?: boolean } = {},
|
|
315
|
+
): any {
|
|
316
|
+
let finalValue: any = value
|
|
317
|
+
let isFloat = false
|
|
318
|
+
const propDefinition = propertyDefinitions[prop]
|
|
319
|
+
|
|
320
|
+
if (typeof value === "string" && !isQuoted) {
|
|
321
|
+
if (finalValue === "true") {
|
|
322
|
+
finalValue = true
|
|
323
|
+
} else if (finalValue === "false") {
|
|
324
|
+
finalValue = false
|
|
325
|
+
} else {
|
|
326
|
+
const asNum = parseInt(value, 10)
|
|
327
|
+
if (!isNaN(asNum)) {
|
|
328
|
+
finalValue = asNum
|
|
329
|
+
} else {
|
|
330
|
+
const asFloat = parseFloat(value)
|
|
331
|
+
if (!isNaN(asFloat)) {
|
|
332
|
+
finalValue = asFloat
|
|
333
|
+
isFloat = true
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const type = propDefinition.type
|
|
339
|
+
finalValue = propDefinition.postParse?.(finalValue) ?? finalValue
|
|
340
|
+
|
|
341
|
+
switch (type) {
|
|
342
|
+
case "integer":
|
|
343
|
+
case "float": {
|
|
344
|
+
if (typeof finalValue !== "number" || (type === "float" && !isFloat) || (type === "integer" && isFloat)) {
|
|
345
|
+
return createTypeError(prop, type, isArray)
|
|
346
|
+
}
|
|
347
|
+
break
|
|
348
|
+
}
|
|
349
|
+
case "string":
|
|
350
|
+
case "boolean": {
|
|
351
|
+
if (typeof finalValue !== propDefinition.type) {
|
|
352
|
+
return createTypeError(prop, type, isArray)
|
|
353
|
+
}
|
|
354
|
+
break
|
|
355
|
+
}
|
|
356
|
+
case "date": {
|
|
357
|
+
if (finalValue instanceof Date) {
|
|
358
|
+
break
|
|
359
|
+
}
|
|
360
|
+
const maybeDate = new Date(finalValue)
|
|
361
|
+
if (isNaN(maybeDate.getTime())) {
|
|
362
|
+
return createTypeError(prop, "date", isArray)
|
|
363
|
+
} else {
|
|
364
|
+
finalValue = maybeDate
|
|
365
|
+
}
|
|
366
|
+
break
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return finalValue
|
|
371
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { Parser } from "../Parser.js"
|
|
2
|
+
import type { Position } from "../types/ast.js"
|
|
3
|
+
import type { ValueQuery } from "../types/parser.js"
|
|
4
|
+
|
|
1
5
|
/* TODO TOUPDATE */
|
|
2
6
|
/**
|
|
3
7
|
* A pre-configured parser for parsing shortcut contexts (similar to VSCode's [when clause contexts](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts)).
|
|
@@ -9,15 +13,11 @@
|
|
|
9
13
|
* The validate function will return a list of positions with a list of errors which includes handling invalid or duplicate regex flags.
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
|
-
import { Parser } from "../Parser.js"
|
|
13
|
-
import type { Position } from "../types/ast.js"
|
|
14
|
-
import type { ValueQuery } from "../types/parser.js"
|
|
15
|
-
|
|
16
16
|
|
|
17
|
-
export class ShortcutContextParser<
|
|
17
|
+
export class ShortcutContextParser<TErrorTokens extends
|
|
18
18
|
Position & { type: ("invalidKey" | "unregexableKey" | "invalidRegexFlag" | "duplicateRegexFlag") } =
|
|
19
19
|
Position & { type: ("invalidKey" | "unregexableKey" | "invalidRegexFlag" | "duplicateRegexFlag") },
|
|
20
|
-
> extends Parser<
|
|
20
|
+
> extends Parser<TErrorTokens> {
|
|
21
21
|
validKeys: string[] = []
|
|
22
22
|
|
|
23
23
|
regexablekeys: string[] = []
|
|
@@ -49,21 +49,21 @@ export class ShortcutContextParser<T extends
|
|
|
49
49
|
}
|
|
50
50
|
return contextValue === condition.value
|
|
51
51
|
},
|
|
52
|
-
valueValidator: (_contextValue, query):
|
|
53
|
-
let tokens:
|
|
52
|
+
valueValidator: (_contextValue, query): TErrorTokens[] | void => {
|
|
53
|
+
let tokens: TErrorTokens[] = []
|
|
54
54
|
if (!this.validKeys.includes(query.propertyName!)) {
|
|
55
55
|
tokens = tokens.concat(query.property.map(token => ({
|
|
56
56
|
start: token.start,
|
|
57
57
|
end: token.end,
|
|
58
58
|
type: "invalidKey",
|
|
59
|
-
})) as
|
|
59
|
+
})) as TErrorTokens[])
|
|
60
60
|
}
|
|
61
61
|
if (query.isRegex && !this.regexablekeys.includes(query.propertyName!)) {
|
|
62
62
|
tokens = tokens.concat(query.property.map(token => ({
|
|
63
63
|
start: token.start,
|
|
64
64
|
end: token.end,
|
|
65
65
|
type: "unregexableKey",
|
|
66
|
-
})) as
|
|
66
|
+
})) as TErrorTokens[])
|
|
67
67
|
}
|
|
68
68
|
if (query.regexFlags) {
|
|
69
69
|
const chars = query.regexFlags.value.split("")
|
|
@@ -75,14 +75,14 @@ export class ShortcutContextParser<T extends
|
|
|
75
75
|
start: start + i,
|
|
76
76
|
end: start + i + 1,
|
|
77
77
|
type: "duplicateRegexFlag",
|
|
78
|
-
} as
|
|
78
|
+
} as TErrorTokens)
|
|
79
79
|
}
|
|
80
80
|
if (!validRegexFlags.includes(char)) {
|
|
81
81
|
tokens.push({
|
|
82
82
|
start: start + i,
|
|
83
83
|
end: start + i + 1,
|
|
84
84
|
type: "invalidRegexFlag",
|
|
85
|
-
} as
|
|
85
|
+
} as TErrorTokens)
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -93,7 +93,7 @@ export class ShortcutContextParser<T extends
|
|
|
93
93
|
let finalOperator: any = operator
|
|
94
94
|
// another way to allow special unquoted value types is something like this:
|
|
95
95
|
if (typeof value === "string" && !isQuoted) {
|
|
96
|
-
const asNum = parseInt(value,
|
|
96
|
+
const asNum = parseInt(value, 10)
|
|
97
97
|
if (!isNaN(asNum)) finalValue = asNum
|
|
98
98
|
if (["true", "false"].includes(value)) {
|
|
99
99
|
finalValue = value === "true"
|
|
@@ -132,7 +132,7 @@ export class ShortcutContextParser<T extends
|
|
|
132
132
|
for (const key of Object.keys(context)) {
|
|
133
133
|
if (typeof context[key] === "boolean") {
|
|
134
134
|
this.validKeys.push(prev ? `${prev}.${key}` : key)
|
|
135
|
-
if (context[key]
|
|
135
|
+
if (context[key]) {
|
|
136
136
|
this.regexablekeys.push(prev ? `${prev}.${key}` : key)
|
|
137
137
|
}
|
|
138
138
|
} else {
|
package/src/examples/index.ts
CHANGED
|
@@ -3,12 +3,12 @@ import { indent } from "@alanscodelog/utils/indent.js"
|
|
|
3
3
|
import { pretty } from "@alanscodelog/utils/pretty.js"
|
|
4
4
|
import type { Keys } from "@alanscodelog/utils/types"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import type {
|
|
6
|
+
import packageJson from "../../package.json" with { type: "json" }
|
|
7
|
+
const { version, repository } = packageJson
|
|
8
|
+
import type { ErrorInfo, ParserError } from "../types/errors.js"
|
|
9
9
|
|
|
10
10
|
/** @internal */
|
|
11
|
-
export class ExpressitError<T extends
|
|
11
|
+
export class ExpressitError<T extends ParserError> extends Error {
|
|
12
12
|
version: string = version
|
|
13
13
|
|
|
14
14
|
repo: string = repository
|