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
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 QAST Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# QAST — Query to AST to ORM
|
|
2
|
+
|
|
3
|
+
**QAST** is a small, ORM-agnostic library that parses human-readable query strings (e.g. `age gt 25 and (name eq "John" or city eq "Paris")`) into an **Abstract Syntax Tree (AST)** and then transforms that AST into **ORM-compatible filter objects** such as Prisma or TypeORM filters.
|
|
4
|
+
|
|
5
|
+
It aims to provide a secure, declarative, and type-safe way to support advanced filtering in REST APIs — without falling into the pitfalls of raw string-based query patterns.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🔒 **Safe**: Validates operators, values, and fields against whitelists
|
|
10
|
+
- 🎯 **Type-Safe**: Full TypeScript support for parsed ASTs and generated filters
|
|
11
|
+
- 🔌 **ORM-Agnostic**: Works with Prisma, TypeORM, Sequelize, and more via adapters
|
|
12
|
+
- 📝 **Simple Syntax**: Natural query expressions using logical operators
|
|
13
|
+
- 🚀 **Lightweight**: No dependencies, small bundle size
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install qast
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Basic Usage
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { parseQuery, toPrismaFilter } from 'qast';
|
|
27
|
+
|
|
28
|
+
const query = 'age gt 25 and (name eq "John" or city eq "Paris")';
|
|
29
|
+
|
|
30
|
+
const ast = parseQuery(query);
|
|
31
|
+
const prismaFilter = toPrismaFilter(ast);
|
|
32
|
+
|
|
33
|
+
await prisma.user.findMany(prismaFilter);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### With Validation
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { parseQuery, toPrismaFilter } from 'qast';
|
|
40
|
+
|
|
41
|
+
const query = 'age gt 25 and name eq "John"';
|
|
42
|
+
|
|
43
|
+
// Parse with whitelist validation
|
|
44
|
+
const ast = parseQuery(query, {
|
|
45
|
+
allowedFields: ['age', 'name', 'city'],
|
|
46
|
+
allowedOperators: ['gt', 'eq', 'lt'],
|
|
47
|
+
validate: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const prismaFilter = toPrismaFilter(ast);
|
|
51
|
+
await prisma.user.findMany(prismaFilter);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Query Syntax
|
|
55
|
+
|
|
56
|
+
### Operators
|
|
57
|
+
|
|
58
|
+
QAST supports the following comparison operators:
|
|
59
|
+
|
|
60
|
+
- `eq` - Equal
|
|
61
|
+
- `ne` - Not equal
|
|
62
|
+
- `gt` - Greater than
|
|
63
|
+
- `lt` - Less than
|
|
64
|
+
- `gte` - Greater than or equal
|
|
65
|
+
- `lte` - Less than or equal
|
|
66
|
+
- `in` - In array
|
|
67
|
+
- `contains` - Contains substring (string matching)
|
|
68
|
+
|
|
69
|
+
### Logical Operators
|
|
70
|
+
|
|
71
|
+
- `and` - Logical AND
|
|
72
|
+
- `or` - Logical OR
|
|
73
|
+
|
|
74
|
+
### Values
|
|
75
|
+
|
|
76
|
+
- **Strings**: Use single or double quotes: `"John"` or `'John'`
|
|
77
|
+
- **Numbers**: Integers or floats: `25`, `25.99`, `-10`
|
|
78
|
+
- **Booleans**: `true` or `false`
|
|
79
|
+
- **Arrays**: For `in` operator: `[1,2,3]` or `["John","Jane"]`
|
|
80
|
+
|
|
81
|
+
### Examples
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// Simple comparison
|
|
85
|
+
'age gt 25'
|
|
86
|
+
|
|
87
|
+
// String comparison
|
|
88
|
+
'name eq "John"'
|
|
89
|
+
|
|
90
|
+
// Boolean comparison
|
|
91
|
+
'active eq true'
|
|
92
|
+
|
|
93
|
+
// Array (in operator)
|
|
94
|
+
'age in [1,2,3]'
|
|
95
|
+
|
|
96
|
+
// AND operation
|
|
97
|
+
'age gt 25 and name eq "John"'
|
|
98
|
+
|
|
99
|
+
// OR operation
|
|
100
|
+
'name eq "John" or name eq "Jane"'
|
|
101
|
+
|
|
102
|
+
// Nested parentheses
|
|
103
|
+
'age gt 25 and (name eq "John" or city eq "Paris")'
|
|
104
|
+
|
|
105
|
+
// Complex query
|
|
106
|
+
'age gt 25 and (name eq "John" or city eq "Paris") and active eq true'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## ORM Adapters
|
|
110
|
+
|
|
111
|
+
### Prisma
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { parseQuery, toPrismaFilter } from 'qast';
|
|
115
|
+
|
|
116
|
+
const query = 'age gt 25 and name eq "John"';
|
|
117
|
+
const ast = parseQuery(query);
|
|
118
|
+
const filter = toPrismaFilter(ast);
|
|
119
|
+
|
|
120
|
+
// filter = {
|
|
121
|
+
// where: {
|
|
122
|
+
// age: { gt: 25 },
|
|
123
|
+
// name: { equals: "John" }
|
|
124
|
+
// }
|
|
125
|
+
// }
|
|
126
|
+
|
|
127
|
+
await prisma.user.findMany(filter);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### TypeORM
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { parseQuery, toTypeORMFilter } from 'qast';
|
|
134
|
+
import { MoreThan, Equal } from 'typeorm';
|
|
135
|
+
|
|
136
|
+
const query = 'age gt 25 and name eq "John"';
|
|
137
|
+
const ast = parseQuery(query);
|
|
138
|
+
const filter = toTypeORMFilter(ast);
|
|
139
|
+
|
|
140
|
+
// Note: TypeORM requires operator functions for non-equality comparisons
|
|
141
|
+
// The adapter returns a structure that you can transform using TypeORM operators
|
|
142
|
+
// For equality, TypeORM accepts plain values directly
|
|
143
|
+
|
|
144
|
+
// filter.where = {
|
|
145
|
+
// age: { __qast_operator__: 'gt', value: 25 },
|
|
146
|
+
// name: "John"
|
|
147
|
+
// }
|
|
148
|
+
|
|
149
|
+
// Transform to use TypeORM operators:
|
|
150
|
+
// const transformed = {
|
|
151
|
+
// age: MoreThan(25),
|
|
152
|
+
// name: "John"
|
|
153
|
+
// }
|
|
154
|
+
|
|
155
|
+
await userRepository.find({ where: transformed });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Note**: TypeORM requires operator functions (`MoreThan`, `LessThan`, etc.) for non-equality comparisons. The adapter returns a structure with metadata that you can transform. For equality comparisons, TypeORM accepts plain values.
|
|
159
|
+
|
|
160
|
+
### Sequelize
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { parseQuery, toSequelizeFilter } from 'qast';
|
|
164
|
+
import { Op } from 'sequelize';
|
|
165
|
+
|
|
166
|
+
const query = 'age gt 25 and name eq "John"';
|
|
167
|
+
const ast = parseQuery(query);
|
|
168
|
+
const filter = toSequelizeFilter(ast);
|
|
169
|
+
|
|
170
|
+
// filter = {
|
|
171
|
+
// __qast_logical__: 'and',
|
|
172
|
+
// conditions: [
|
|
173
|
+
// { age: { __qast_operator__: 'gt', value: 25 } },
|
|
174
|
+
// { name: 'John' }
|
|
175
|
+
// ]
|
|
176
|
+
// }
|
|
177
|
+
|
|
178
|
+
// Transform to use Sequelize Op operators:
|
|
179
|
+
function transformSequelizeFilter(filter: any): any {
|
|
180
|
+
if (filter.__qast_logical__) {
|
|
181
|
+
const op = filter.__qast_logical__ === 'and' ? Op.and : Op.or;
|
|
182
|
+
return {
|
|
183
|
+
[op]: filter.conditions.map(transformSequelizeFilter),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const result: any = {};
|
|
188
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
189
|
+
if (value && typeof value === 'object' && '__qast_operator__' in value) {
|
|
190
|
+
const opKey = value.__qast_operator__;
|
|
191
|
+
const op = Op[opKey as keyof typeof Op];
|
|
192
|
+
if (opKey === 'contains') {
|
|
193
|
+
result[key] = { [Op.like]: `%${value.value}%` };
|
|
194
|
+
} else {
|
|
195
|
+
result[key] = { [op]: value.value };
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
result[key] = value;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const transformed = transformSequelizeFilter(filter);
|
|
205
|
+
// transformed = {
|
|
206
|
+
// [Op.and]: [
|
|
207
|
+
// { age: { [Op.gt]: 25 } },
|
|
208
|
+
// { name: 'John' }
|
|
209
|
+
// ]
|
|
210
|
+
// }
|
|
211
|
+
|
|
212
|
+
await User.findAll({ where: transformed });
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Note**: Sequelize uses the `Op` object from 'sequelize'. Since Sequelize is an optional peer dependency, the adapter returns a structure with metadata (`__qast_operator__` and `__qast_logical__`) that you need to transform to use `Op` operators. For simple equality (`eq`), the adapter returns plain values which Sequelize accepts directly.
|
|
216
|
+
|
|
217
|
+
## API Reference
|
|
218
|
+
|
|
219
|
+
### `parseQuery(query: string, options?: ParseOptions): QastNode`
|
|
220
|
+
|
|
221
|
+
Parse a query string into an AST.
|
|
222
|
+
|
|
223
|
+
**Parameters:**
|
|
224
|
+
- `query` - The query string to parse
|
|
225
|
+
- `options` - Optional parsing options:
|
|
226
|
+
- `allowedFields?: string[]` - Whitelist of allowed field names
|
|
227
|
+
- `allowedOperators?: Operator[]` - Whitelist of allowed operators
|
|
228
|
+
- `validate?: boolean` - Whether to validate against whitelists (default: true if whitelists are provided)
|
|
229
|
+
|
|
230
|
+
**Returns:** The parsed AST node
|
|
231
|
+
|
|
232
|
+
**Example:**
|
|
233
|
+
```typescript
|
|
234
|
+
const ast = parseQuery('age gt 25', {
|
|
235
|
+
allowedFields: ['age', 'name'],
|
|
236
|
+
allowedOperators: ['gt', 'eq'],
|
|
237
|
+
validate: true,
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### `toPrismaFilter(ast: QastNode): PrismaFilter`
|
|
242
|
+
|
|
243
|
+
Transform an AST to a Prisma filter.
|
|
244
|
+
|
|
245
|
+
**Returns:** Prisma filter object with `where` property
|
|
246
|
+
|
|
247
|
+
### `toTypeORMFilter(ast: QastNode): TypeORMFilter`
|
|
248
|
+
|
|
249
|
+
Transform an AST to a TypeORM filter.
|
|
250
|
+
|
|
251
|
+
**Returns:** TypeORM filter object with `where` property
|
|
252
|
+
|
|
253
|
+
**Note:** TypeORM requires operator functions for non-equality comparisons. You may need to transform the result.
|
|
254
|
+
|
|
255
|
+
### `toSequelizeFilter(ast: QastNode): SequelizeFilter`
|
|
256
|
+
|
|
257
|
+
Transform an AST to a Sequelize filter.
|
|
258
|
+
|
|
259
|
+
**Returns:** Sequelize filter object
|
|
260
|
+
|
|
261
|
+
**Note:** Sequelize uses the `Op` object. You need to transform `$`-prefixed operators to use `Op` operators.
|
|
262
|
+
|
|
263
|
+
### `validateQuery(ast: QastNode, whitelist: WhitelistOptions): void`
|
|
264
|
+
|
|
265
|
+
Validate an AST against whitelists.
|
|
266
|
+
|
|
267
|
+
**Parameters:**
|
|
268
|
+
- `ast` - The AST to validate
|
|
269
|
+
- `whitelist` - Whitelist options:
|
|
270
|
+
- `allowedFields?: string[]` - Allowed field names
|
|
271
|
+
- `allowedOperators?: Operator[]` - Allowed operators
|
|
272
|
+
|
|
273
|
+
**Throws:** `ValidationError` if validation fails
|
|
274
|
+
|
|
275
|
+
### `extractFields(ast: QastNode): string[]`
|
|
276
|
+
|
|
277
|
+
Extract all field names used in an AST.
|
|
278
|
+
|
|
279
|
+
**Returns:** Array of unique field names
|
|
280
|
+
|
|
281
|
+
### `extractOperators(ast: QastNode): Operator[]`
|
|
282
|
+
|
|
283
|
+
Extract all operators used in an AST.
|
|
284
|
+
|
|
285
|
+
**Returns:** Array of unique operators
|
|
286
|
+
|
|
287
|
+
## Security Best Practices
|
|
288
|
+
|
|
289
|
+
1. **Always use whitelists**: Restrict which fields and operators can be used in queries.
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
const ast = parseQuery(req.query.filter, {
|
|
293
|
+
allowedFields: ['age', 'name', 'city'],
|
|
294
|
+
allowedOperators: ['gt', 'eq', 'lt'],
|
|
295
|
+
validate: true,
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
2. **Validate user input**: Don't trust user-provided query strings without validation.
|
|
300
|
+
|
|
301
|
+
3. **Limit query complexity**: Consider limiting the depth of nested queries to prevent DoS attacks.
|
|
302
|
+
|
|
303
|
+
4. **Use type checking**: Ensure values match expected types for fields.
|
|
304
|
+
|
|
305
|
+
## Error Handling
|
|
306
|
+
|
|
307
|
+
QAST provides custom error classes:
|
|
308
|
+
|
|
309
|
+
- `ParseError` - Syntax errors in query strings
|
|
310
|
+
- `ValidationError` - Validation failures (disallowed fields/operators)
|
|
311
|
+
- `TokenizationError` - Tokenization errors
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import { parseQuery, ParseError, ValidationError } from 'qast';
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const ast = parseQuery(query, { allowedFields: ['age'], validate: true });
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (error instanceof ParseError) {
|
|
320
|
+
console.error('Parse error:', error.message);
|
|
321
|
+
} else if (error instanceof ValidationError) {
|
|
322
|
+
console.error('Validation error:', error.message);
|
|
323
|
+
console.error('Field:', error.field);
|
|
324
|
+
console.error('Operator:', error.operator);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## TypeScript Support
|
|
330
|
+
|
|
331
|
+
QAST is written in TypeScript and provides full type definitions:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { QastNode, ComparisonNode, LogicalNode, Operator } from 'qast';
|
|
335
|
+
|
|
336
|
+
function processNode(node: QastNode): void {
|
|
337
|
+
if (node.type === 'COMPARISON') {
|
|
338
|
+
const comparison = node as ComparisonNode;
|
|
339
|
+
console.log(comparison.field, comparison.op, comparison.value);
|
|
340
|
+
} else if (node.type === 'AND' || node.type === 'OR') {
|
|
341
|
+
const logical = node as LogicalNode;
|
|
342
|
+
processNode(logical.left);
|
|
343
|
+
processNode(logical.right);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Examples
|
|
349
|
+
|
|
350
|
+
### REST API Endpoint
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { parseQuery, toPrismaFilter } from 'qast';
|
|
354
|
+
import { PrismaClient } from '@prisma/client';
|
|
355
|
+
|
|
356
|
+
const prisma = new PrismaClient();
|
|
357
|
+
|
|
358
|
+
app.get('/users', async (req, res) => {
|
|
359
|
+
try {
|
|
360
|
+
const query = req.query.filter as string;
|
|
361
|
+
|
|
362
|
+
// Parse and validate query
|
|
363
|
+
const ast = parseQuery(query, {
|
|
364
|
+
allowedFields: ['age', 'name', 'city', 'active'],
|
|
365
|
+
allowedOperators: ['gt', 'lt', 'eq', 'in'],
|
|
366
|
+
validate: true,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Transform to Prisma filter
|
|
370
|
+
const filter = toPrismaFilter(ast);
|
|
371
|
+
|
|
372
|
+
// Query database
|
|
373
|
+
const users = await prisma.user.findMany(filter);
|
|
374
|
+
|
|
375
|
+
res.json(users);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (error instanceof ValidationError) {
|
|
378
|
+
res.status(400).json({ error: error.message });
|
|
379
|
+
} else {
|
|
380
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Express Middleware
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import { parseQuery, toPrismaFilter, ValidationError } from 'qast';
|
|
390
|
+
|
|
391
|
+
function qastMiddleware(allowedFields: string[], allowedOperators: Operator[]) {
|
|
392
|
+
return (req, res, next) => {
|
|
393
|
+
try {
|
|
394
|
+
if (req.query.filter) {
|
|
395
|
+
const ast = parseQuery(req.query.filter, {
|
|
396
|
+
allowedFields,
|
|
397
|
+
allowedOperators,
|
|
398
|
+
validate: true,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
req.qastFilter = toPrismaFilter(ast);
|
|
402
|
+
}
|
|
403
|
+
next();
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (error instanceof ValidationError) {
|
|
406
|
+
res.status(400).json({ error: error.message });
|
|
407
|
+
} else {
|
|
408
|
+
next(error);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## License
|
|
416
|
+
|
|
417
|
+
MIT © 2025
|
|
418
|
+
|
|
419
|
+
## Contributing
|
|
420
|
+
|
|
421
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
422
|
+
|
|
423
|
+
- GitHub Repository: https://github.com/hocestnonsatis/qast
|
|
424
|
+
- Issues: https://github.com/hocestnonsatis/qast/issues
|
|
425
|
+
|
|
426
|
+
## Acknowledgments
|
|
427
|
+
|
|
428
|
+
QAST is inspired by the need for safe, type-safe query parsing in REST APIs. It aims to provide a lightweight alternative to complex query protocols while maintaining security and developer experience.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { QastNode } from '../types/ast';
|
|
2
|
+
/**
|
|
3
|
+
* Prisma filter type
|
|
4
|
+
*/
|
|
5
|
+
export type PrismaFilter = {
|
|
6
|
+
where: Record<string, any>;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Transform a QAST AST node to a Prisma filter
|
|
10
|
+
*/
|
|
11
|
+
export declare function toPrismaFilter(ast: QastNode): PrismaFilter;
|
|
12
|
+
//# sourceMappingURL=prisma.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["../../src/adapters/prisma.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAgE,MAAM,cAAc,CAAC;AAEtG;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC5B,CAAC;AAEF;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,QAAQ,GAAG,YAAY,CAI1D"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toPrismaFilter = toPrismaFilter;
|
|
4
|
+
const ast_1 = require("../types/ast");
|
|
5
|
+
/**
|
|
6
|
+
* Transform a QAST AST node to a Prisma filter
|
|
7
|
+
*/
|
|
8
|
+
function toPrismaFilter(ast) {
|
|
9
|
+
return {
|
|
10
|
+
where: transformNode(ast),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Transform a node to Prisma format
|
|
15
|
+
*/
|
|
16
|
+
function transformNode(node) {
|
|
17
|
+
if ((0, ast_1.isComparisonNode)(node)) {
|
|
18
|
+
return transformComparisonNode(node);
|
|
19
|
+
}
|
|
20
|
+
else if ((0, ast_1.isLogicalNode)(node)) {
|
|
21
|
+
return transformLogicalNode(node);
|
|
22
|
+
}
|
|
23
|
+
throw new Error('Invalid node type');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Transform a comparison node to Prisma format
|
|
27
|
+
*/
|
|
28
|
+
function transformComparisonNode(node) {
|
|
29
|
+
const { field, op, value } = node;
|
|
30
|
+
// Map operators to Prisma operators
|
|
31
|
+
switch (op) {
|
|
32
|
+
case 'eq':
|
|
33
|
+
return { [field]: { equals: value } };
|
|
34
|
+
case 'ne':
|
|
35
|
+
return { [field]: { not: value } };
|
|
36
|
+
case 'gt':
|
|
37
|
+
return { [field]: { gt: value } };
|
|
38
|
+
case 'lt':
|
|
39
|
+
return { [field]: { lt: value } };
|
|
40
|
+
case 'gte':
|
|
41
|
+
return { [field]: { gte: value } };
|
|
42
|
+
case 'lte':
|
|
43
|
+
return { [field]: { lte: value } };
|
|
44
|
+
case 'in':
|
|
45
|
+
return { [field]: { in: value } };
|
|
46
|
+
case 'contains':
|
|
47
|
+
return { [field]: { contains: value } };
|
|
48
|
+
default:
|
|
49
|
+
throw new Error(`Unsupported operator: ${op}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Transform a logical node to Prisma format
|
|
54
|
+
*/
|
|
55
|
+
function transformLogicalNode(node) {
|
|
56
|
+
const leftFilter = transformNode(node.left);
|
|
57
|
+
const rightFilter = transformNode(node.right);
|
|
58
|
+
if (node.type === 'AND') {
|
|
59
|
+
// For AND, merge the filters
|
|
60
|
+
return mergeFilters(leftFilter, rightFilter);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// For OR, create an OR array
|
|
64
|
+
// If either side already has an OR, we need to flatten
|
|
65
|
+
const leftOr = leftFilter.OR;
|
|
66
|
+
const rightOr = rightFilter.OR;
|
|
67
|
+
if (leftOr && rightOr) {
|
|
68
|
+
// Both have OR arrays, combine them
|
|
69
|
+
return {
|
|
70
|
+
OR: [...leftOr, ...rightOr],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
else if (leftOr) {
|
|
74
|
+
// Left has OR, add right to it
|
|
75
|
+
return {
|
|
76
|
+
OR: [...leftOr, rightFilter],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
else if (rightOr) {
|
|
80
|
+
// Right has OR, add left to it
|
|
81
|
+
return {
|
|
82
|
+
OR: [leftFilter, ...rightOr],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Neither has OR, create new OR array
|
|
87
|
+
return {
|
|
88
|
+
OR: [leftFilter, rightFilter],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Merge two Prisma filters (for AND operations)
|
|
95
|
+
*/
|
|
96
|
+
function mergeFilters(left, right) {
|
|
97
|
+
const result = { ...left };
|
|
98
|
+
// Handle OR arrays - if both have OR, we need to combine them properly
|
|
99
|
+
// For AND with OR, Prisma requires: { field: value, OR: [...] }
|
|
100
|
+
if (left.OR || right.OR) {
|
|
101
|
+
// If both sides have OR, we need to distribute
|
|
102
|
+
// This is complex - for now, we'll merge non-OR fields and keep OR separate
|
|
103
|
+
const leftOr = left.OR;
|
|
104
|
+
const rightOr = right.OR;
|
|
105
|
+
// Remove OR from both objects
|
|
106
|
+
const leftWithoutOr = { ...left };
|
|
107
|
+
const rightWithoutOr = { ...right };
|
|
108
|
+
delete leftWithoutOr.OR;
|
|
109
|
+
delete rightWithoutOr.OR;
|
|
110
|
+
// Merge non-OR fields
|
|
111
|
+
Object.assign(result, leftWithoutOr);
|
|
112
|
+
Object.assign(result, rightWithoutOr);
|
|
113
|
+
// Handle OR arrays - in Prisma, when ANDing with OR, we need to be careful
|
|
114
|
+
if (leftOr && rightOr) {
|
|
115
|
+
// This is complex - we'll create nested conditions
|
|
116
|
+
// For simplicity, we'll merge all fields and combine ORs
|
|
117
|
+
result.OR = [...(leftOr || []), ...(rightOr || [])];
|
|
118
|
+
}
|
|
119
|
+
else if (leftOr) {
|
|
120
|
+
result.OR = leftOr;
|
|
121
|
+
}
|
|
122
|
+
else if (rightOr) {
|
|
123
|
+
result.OR = rightOr;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Simple merge - combine all fields
|
|
128
|
+
Object.assign(result, right);
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=prisma.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prisma.js","sourceRoot":"","sources":["../../src/adapters/prisma.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { QastNode } from '../types/ast';
|
|
2
|
+
/**
|
|
3
|
+
* Sequelize filter type
|
|
4
|
+
*
|
|
5
|
+
* Note: Sequelize uses the Op object from 'sequelize' package for operators.
|
|
6
|
+
* Since Sequelize is an optional peer dependency, we cannot import Op directly.
|
|
7
|
+
*
|
|
8
|
+
* This adapter returns a structure that represents the query logic, but you need
|
|
9
|
+
* to transform it to use Sequelize's Op operators.
|
|
10
|
+
*
|
|
11
|
+
* For simple equality, Sequelize accepts plain values: { age: 25 }
|
|
12
|
+
* For other operators, you need Op: { age: { [Op.gt]: 25 } }
|
|
13
|
+
* For logical operations, you need Op.and/Op.or: { [Op.and]: [...] }
|
|
14
|
+
*/
|
|
15
|
+
export type SequelizeFilter = Record<string, any>;
|
|
16
|
+
/**
|
|
17
|
+
* Transform a QAST AST node to a Sequelize filter
|
|
18
|
+
*
|
|
19
|
+
* IMPORTANT: Sequelize uses the Op object from 'sequelize', not $ operators.
|
|
20
|
+
* This adapter returns a structure with metadata that you need to transform.
|
|
21
|
+
*
|
|
22
|
+
* Example usage:
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { Op } from 'sequelize';
|
|
25
|
+
* import { toSequelizeFilter } from 'qast';
|
|
26
|
+
*
|
|
27
|
+
* const filter = toSequelizeFilter(ast);
|
|
28
|
+
* // Returns a structure like:
|
|
29
|
+
* // { age: { __qast_operator__: 'gt', value: 25 } }
|
|
30
|
+
* // You need to transform it:
|
|
31
|
+
* // { age: { [Op.gt]: 25 } }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* For simple equality (eq operator), you can use plain values directly.
|
|
35
|
+
*/
|
|
36
|
+
export declare function toSequelizeFilter(ast: QastNode): SequelizeFilter;
|
|
37
|
+
//# sourceMappingURL=sequelize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sequelize.d.ts","sourceRoot":"","sources":["../../src/adapters/sequelize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAgE,MAAM,cAAc,CAAC;AAEtG;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAElD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,QAAQ,GAAG,eAAe,CAEhE"}
|