qast 1.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 +22 -0
- package/README.md +428 -0
- package/dist/adapters/prisma.d.ts +12 -0
- package/dist/adapters/prisma.d.ts.map +1 -0
- package/dist/adapters/prisma.js +132 -0
- package/dist/adapters/prisma.js.map +1 -0
- package/dist/adapters/sequelize.d.ts +37 -0
- package/dist/adapters/sequelize.d.ts.map +1 -0
- package/dist/adapters/sequelize.js +166 -0
- package/dist/adapters/sequelize.js.map +1 -0
- package/dist/adapters/typeorm.d.ts +18 -0
- package/dist/adapters/typeorm.d.ts.map +1 -0
- package/dist/adapters/typeorm.js +112 -0
- package/dist/adapters/typeorm.js.map +1 -0
- package/dist/errors.d.ts +35 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +61 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/parser.d.ts +49 -0
- package/dist/parser/parser.d.ts.map +1 -0
- package/dist/parser/parser.js +148 -0
- package/dist/parser/parser.js.map +1 -0
- package/dist/parser/tokenizer.d.ts +73 -0
- package/dist/parser/tokenizer.d.ts.map +1 -0
- package/dist/parser/tokenizer.js +352 -0
- package/dist/parser/tokenizer.js.map +1 -0
- package/dist/parser/validator.d.ts +14 -0
- package/dist/parser/validator.d.ts.map +1 -0
- package/dist/parser/validator.js +94 -0
- package/dist/parser/validator.js.map +1 -0
- package/dist/types/ast.d.ts +72 -0
- package/dist/types/ast.d.ts.map +1 -0
- package/dist/types/ast.js +17 -0
- package/dist/types/ast.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateQuery = validateQuery;
|
|
4
|
+
exports.extractFields = extractFields;
|
|
5
|
+
exports.extractOperators = extractOperators;
|
|
6
|
+
const errors_1 = require("../errors");
|
|
7
|
+
const ast_1 = require("../types/ast");
|
|
8
|
+
/**
|
|
9
|
+
* Validate an AST against whitelist options
|
|
10
|
+
*/
|
|
11
|
+
function validateQuery(ast, whitelist) {
|
|
12
|
+
validateNode(ast, whitelist);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Recursively validate a node
|
|
16
|
+
*/
|
|
17
|
+
function validateNode(node, whitelist) {
|
|
18
|
+
if ((0, ast_1.isComparisonNode)(node)) {
|
|
19
|
+
validateComparisonNode(node, whitelist);
|
|
20
|
+
}
|
|
21
|
+
else if ((0, ast_1.isLogicalNode)(node)) {
|
|
22
|
+
validateLogicalNode(node, whitelist);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validate a comparison node
|
|
27
|
+
*/
|
|
28
|
+
function validateComparisonNode(node, whitelist) {
|
|
29
|
+
// Validate field
|
|
30
|
+
if (whitelist.allowedFields && whitelist.allowedFields.length > 0) {
|
|
31
|
+
if (!whitelist.allowedFields.includes(node.field)) {
|
|
32
|
+
throw new errors_1.ValidationError(`Field '${node.field}' is not allowed. Allowed fields: ${whitelist.allowedFields.join(', ')}`, node.field);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Validate operator
|
|
36
|
+
if (whitelist.allowedOperators && whitelist.allowedOperators.length > 0) {
|
|
37
|
+
if (!whitelist.allowedOperators.includes(node.op)) {
|
|
38
|
+
throw new errors_1.ValidationError(`Operator '${node.op}' is not allowed. Allowed operators: ${whitelist.allowedOperators.join(', ')}`, node.field, node.op);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Validate operator-value combination
|
|
42
|
+
if (node.op === 'in' && !Array.isArray(node.value)) {
|
|
43
|
+
throw new errors_1.ValidationError(`Operator 'in' requires an array value, got ${typeof node.value}`, node.field, node.op);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Validate a logical node
|
|
48
|
+
*/
|
|
49
|
+
function validateLogicalNode(node, whitelist) {
|
|
50
|
+
// Recursively validate left and right children
|
|
51
|
+
validateNode(node.left, whitelist);
|
|
52
|
+
validateNode(node.right, whitelist);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extract all fields used in an AST
|
|
56
|
+
*/
|
|
57
|
+
function extractFields(ast) {
|
|
58
|
+
const fields = [];
|
|
59
|
+
extractFieldsRecursive(ast, fields);
|
|
60
|
+
return [...new Set(fields)]; // Return unique fields
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Recursively extract fields from a node
|
|
64
|
+
*/
|
|
65
|
+
function extractFieldsRecursive(node, fields) {
|
|
66
|
+
if ((0, ast_1.isComparisonNode)(node)) {
|
|
67
|
+
fields.push(node.field);
|
|
68
|
+
}
|
|
69
|
+
else if ((0, ast_1.isLogicalNode)(node)) {
|
|
70
|
+
extractFieldsRecursive(node.left, fields);
|
|
71
|
+
extractFieldsRecursive(node.right, fields);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Extract all operators used in an AST
|
|
76
|
+
*/
|
|
77
|
+
function extractOperators(ast) {
|
|
78
|
+
const operators = [];
|
|
79
|
+
extractOperatorsRecursive(ast, operators);
|
|
80
|
+
return [...new Set(operators)]; // Return unique operators
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Recursively extract operators from a node
|
|
84
|
+
*/
|
|
85
|
+
function extractOperatorsRecursive(node, operators) {
|
|
86
|
+
if ((0, ast_1.isComparisonNode)(node)) {
|
|
87
|
+
operators.push(node.op);
|
|
88
|
+
}
|
|
89
|
+
else if ((0, ast_1.isLogicalNode)(node)) {
|
|
90
|
+
extractOperatorsRecursive(node.left, operators);
|
|
91
|
+
extractOperatorsRecursive(node.right, operators);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/parser/validator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported comparison operators
|
|
3
|
+
*/
|
|
4
|
+
export type Operator = 'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte' | 'in' | 'contains';
|
|
5
|
+
/**
|
|
6
|
+
* Supported value types in comparisons
|
|
7
|
+
*/
|
|
8
|
+
export type QastValue = string | number | boolean | string[] | number[];
|
|
9
|
+
/**
|
|
10
|
+
* Logical operator type
|
|
11
|
+
*/
|
|
12
|
+
export type LogicalOperator = 'AND' | 'OR';
|
|
13
|
+
/**
|
|
14
|
+
* Comparison node representing a field comparison operation
|
|
15
|
+
*/
|
|
16
|
+
export interface ComparisonNode {
|
|
17
|
+
type: 'COMPARISON';
|
|
18
|
+
field: string;
|
|
19
|
+
op: Operator;
|
|
20
|
+
value: QastValue;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Logical node representing a logical operation (AND/OR) between two nodes
|
|
24
|
+
*/
|
|
25
|
+
export interface LogicalNode {
|
|
26
|
+
type: LogicalOperator;
|
|
27
|
+
left: QastNode;
|
|
28
|
+
right: QastNode;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Root AST node type - can be either a comparison or a logical operation
|
|
32
|
+
*/
|
|
33
|
+
export type QastNode = LogicalNode | ComparisonNode;
|
|
34
|
+
/**
|
|
35
|
+
* Type guard to check if a node is a ComparisonNode
|
|
36
|
+
*/
|
|
37
|
+
export declare function isComparisonNode(node: QastNode): node is ComparisonNode;
|
|
38
|
+
/**
|
|
39
|
+
* Type guard to check if a node is a LogicalNode
|
|
40
|
+
*/
|
|
41
|
+
export declare function isLogicalNode(node: QastNode): node is LogicalNode;
|
|
42
|
+
/**
|
|
43
|
+
* Options for parsing queries
|
|
44
|
+
*/
|
|
45
|
+
export interface ParseOptions {
|
|
46
|
+
/**
|
|
47
|
+
* List of allowed field names. If provided, only these fields can be used in queries.
|
|
48
|
+
*/
|
|
49
|
+
allowedFields?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* List of allowed operators. If provided, only these operators can be used in queries.
|
|
52
|
+
*/
|
|
53
|
+
allowedOperators?: Operator[];
|
|
54
|
+
/**
|
|
55
|
+
* Whether to validate the query against whitelists after parsing
|
|
56
|
+
*/
|
|
57
|
+
validate?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Options for query validation
|
|
61
|
+
*/
|
|
62
|
+
export interface WhitelistOptions {
|
|
63
|
+
/**
|
|
64
|
+
* List of allowed field names
|
|
65
|
+
*/
|
|
66
|
+
allowedFields?: string[];
|
|
67
|
+
/**
|
|
68
|
+
* List of allowed operators
|
|
69
|
+
*/
|
|
70
|
+
allowedOperators?: Operator[];
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=ast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/types/ast.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,GAAG,UAAU,CAAC;AAErF;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;AAExE;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,IAAI,CAAC;AAE3C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,QAAQ,CAAC;IACb,KAAK,EAAE,SAAS,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,eAAe,CAAC;IACtB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,QAAQ,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG,cAAc,CAAC;AAEpD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,IAAI,cAAc,CAEvE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,IAAI,WAAW,CAEjE;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAEzB;;OAEG;IACH,gBAAgB,CAAC,EAAE,QAAQ,EAAE,CAAC;IAE9B;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAEzB;;OAEG;IACH,gBAAgB,CAAC,EAAE,QAAQ,EAAE,CAAC;CAC/B"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isComparisonNode = isComparisonNode;
|
|
4
|
+
exports.isLogicalNode = isLogicalNode;
|
|
5
|
+
/**
|
|
6
|
+
* Type guard to check if a node is a ComparisonNode
|
|
7
|
+
*/
|
|
8
|
+
function isComparisonNode(node) {
|
|
9
|
+
return node.type === 'COMPARISON';
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Type guard to check if a node is a LogicalNode
|
|
13
|
+
*/
|
|
14
|
+
function isLogicalNode(node) {
|
|
15
|
+
return node.type === 'AND' || node.type === 'OR';
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=ast.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast.js","sourceRoot":"","sources":["../../src/types/ast.ts"],"names":[],"mappings":";;AA0CA,4CAEC;AAKD,sCAEC;AAZD;;GAEG;AACH,SAAgB,gBAAgB,CAAC,IAAc;IAC7C,OAAO,IAAI,CAAC,IAAI,KAAK,YAAY,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,SAAgB,aAAa,CAAC,IAAc;IAC1C,OAAO,IAAI,CAAC,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;AACnD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qast",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Query to AST to ORM - Parse human-readable query strings into AST and transform them into ORM-compatible filters",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"test": "jest",
|
|
15
|
+
"test:watch": "jest --watch",
|
|
16
|
+
"test:coverage": "jest --coverage",
|
|
17
|
+
"prepare": "npm run build",
|
|
18
|
+
"prepublishOnly": "npm run build && npm test"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"query",
|
|
22
|
+
"parser",
|
|
23
|
+
"ast",
|
|
24
|
+
"orm",
|
|
25
|
+
"prisma",
|
|
26
|
+
"typeorm",
|
|
27
|
+
"sequelize",
|
|
28
|
+
"filter",
|
|
29
|
+
"typescript",
|
|
30
|
+
"query-string",
|
|
31
|
+
"filtering",
|
|
32
|
+
"api"
|
|
33
|
+
],
|
|
34
|
+
"author": "https://x.com/hocestnonsatis",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/hocestnonsatis/qast.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/hocestnonsatis/qast/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/hocestnonsatis/qast#readme",
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=14.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/jest": "^29.5.0",
|
|
49
|
+
"@types/node": "^20.0.0",
|
|
50
|
+
"jest": "^29.5.0",
|
|
51
|
+
"ts-jest": "^29.1.0",
|
|
52
|
+
"typescript": "^5.0.0"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@prisma/client": "*",
|
|
57
|
+
"typeorm": "*",
|
|
58
|
+
"sequelize": "*"
|
|
59
|
+
},
|
|
60
|
+
"peerDependenciesMeta": {
|
|
61
|
+
"@prisma/client": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"typeorm": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"sequelize": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|