@yesiree/jsonq 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 +21 -0
- package/README.md +891 -0
- package/dist/cli.mjs +420 -0
- package/dist/index.cjs +394 -0
- package/dist/index.mjs +390 -0
- package/dist/index.umd.js +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
# jsonq
|
|
2
|
+
|
|
3
|
+
Developer documentation for contributors.
|
|
4
|
+
|
|
5
|
+
> **User documentation:** See [DOCS.md](DOCS.md)
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
jsonq is a lightweight JSON query engine that implements a custom expression language for querying and transforming JSON data. The library provides a functional programming interface with method chaining, similar to JSONPath but with richer expression support.
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
### Core Components
|
|
14
|
+
|
|
15
|
+
**Tokenizer** (`tokenizePipeline`)
|
|
16
|
+
|
|
17
|
+
- Parses query strings into token streams
|
|
18
|
+
- Handles root reference (`$`), property access (`.prop`), array indexing (`[0]`), and method calls (`.method()`)
|
|
19
|
+
- Tracks token positions for extracting method parameters
|
|
20
|
+
|
|
21
|
+
**Evaluator** (`evaluatePipeline`)
|
|
22
|
+
|
|
23
|
+
- Executes token pipeline against data
|
|
24
|
+
- Maintains current value through transformations
|
|
25
|
+
- Handles nested data navigation
|
|
26
|
+
|
|
27
|
+
**Expression Parser** (`parseExpr`)
|
|
28
|
+
|
|
29
|
+
- Recursive descent parser for expressions
|
|
30
|
+
- Generates AST with nodes: literal, variable, property, arrayIndex, unary, binary, methodCall
|
|
31
|
+
- Supports operators: arithmetic, comparison, logical
|
|
32
|
+
|
|
33
|
+
**Expression Evaluator** (`evalExpr`, `evalNode`)
|
|
34
|
+
|
|
35
|
+
- Detects arrow function syntax with regex
|
|
36
|
+
- Creates variable bindings for named parameters
|
|
37
|
+
- Evaluates AST nodes with context (data, item, index, bindings)
|
|
38
|
+
|
|
39
|
+
**Method Executor** (`applyMethod`)
|
|
40
|
+
|
|
41
|
+
- Switch statement dispatching to method implementations
|
|
42
|
+
- Propagates bindings through nested method calls
|
|
43
|
+
- Handles recursive evaluation for nested transformations
|
|
44
|
+
|
|
45
|
+
## Public API
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
export { query };
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**query(expression, data)**
|
|
52
|
+
|
|
53
|
+
- Main entry point
|
|
54
|
+
- Returns transformed data based on expression
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
### Setup
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install
|
|
62
|
+
npm test
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Running Tests
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm test
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Test suite includes 98 tests covering:
|
|
72
|
+
|
|
73
|
+
- Tokenization and parsing
|
|
74
|
+
- All methods (map, filter, sort, etc.)
|
|
75
|
+
- Expression evaluation (arithmetic, logical, comparison)
|
|
76
|
+
- Nested method calls
|
|
77
|
+
- Arrow function syntax and underscore shorthand
|
|
78
|
+
- Root ($) and array ($array) variable access
|
|
79
|
+
- Edge cases and error handling
|
|
80
|
+
|
|
81
|
+
### Building
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm run build
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Outputs to `dist/`:
|
|
88
|
+
|
|
89
|
+
- `index.cjs` - CommonJS
|
|
90
|
+
- `index.mjs` - ES Module
|
|
91
|
+
- `index.umd.js` - UMD (browser)
|
|
92
|
+
- `index.d.ts` - TypeScript definitions
|
|
93
|
+
|
|
94
|
+
## Implementation Details
|
|
95
|
+
|
|
96
|
+
### Supported Methods
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
const METHODS = [
|
|
100
|
+
"map",
|
|
101
|
+
"filter",
|
|
102
|
+
"join",
|
|
103
|
+
"sort",
|
|
104
|
+
"unique",
|
|
105
|
+
"first",
|
|
106
|
+
"last",
|
|
107
|
+
"count",
|
|
108
|
+
"sum",
|
|
109
|
+
"avg",
|
|
110
|
+
"min",
|
|
111
|
+
"max",
|
|
112
|
+
];
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Token Types
|
|
116
|
+
|
|
117
|
+
- `root` - `$` reference
|
|
118
|
+
- `property` - `.prop` access
|
|
119
|
+
- `arrayIndex` - `[0]` access
|
|
120
|
+
- `method` - `.method()` call with params
|
|
121
|
+
|
|
122
|
+
### Expression Syntax Features
|
|
123
|
+
|
|
124
|
+
**Variable syntaxes:**
|
|
125
|
+
|
|
126
|
+
1. Root reference: `$`
|
|
127
|
+
2. Built-in variables: `$item`, `$index`, `$array`
|
|
128
|
+
3. Underscore shorthand: `_` (alias for `$item`)
|
|
129
|
+
4. Arrow functions: `x => x.prop` (creates binding for `x`)
|
|
130
|
+
|
|
131
|
+
**Arrow function detection:**
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
const arrowMatch = expr.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*=>\s*(.+)$/);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Binding propagation:**
|
|
138
|
+
Arrow function bindings are passed through nested method calls, enabling parent scope access in nested iterations.
|
|
139
|
+
|
|
140
|
+
### AST Node Types
|
|
141
|
+
|
|
142
|
+
- `literal` - Numbers, strings, booleans, null, undefined
|
|
143
|
+
- `variable` - `$`, `$item`, `$index`, `$array`, `_`, or named params
|
|
144
|
+
- `property` - Property access (`obj.prop`)
|
|
145
|
+
- `arrayIndex` - Array indexing (`arr[0]`)
|
|
146
|
+
- `unary` - Unary operators (`!`, `-`)
|
|
147
|
+
- `binary` - Binary operators (arithmetic, comparison, logical)
|
|
148
|
+
- `methodCall` - Nested method invocations
|
|
149
|
+
|
|
150
|
+
### Sort Implementation
|
|
151
|
+
|
|
152
|
+
Descending sort detected by `-` prefix:
|
|
153
|
+
|
|
154
|
+
```javascript
|
|
155
|
+
const descending = expr.startsWith("-");
|
|
156
|
+
const sortExpr = descending ? expr.substring(1) : expr;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Date Handling
|
|
160
|
+
|
|
161
|
+
Dates prefixed with `@` are parsed to timestamps:
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
if (token.startsWith("@")) {
|
|
165
|
+
return new Date(token.substring(1)).getTime();
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Contributing
|
|
170
|
+
|
|
171
|
+
### Code Style
|
|
172
|
+
|
|
173
|
+
- No comments in implementation code
|
|
174
|
+
- Variable names provide documentation
|
|
175
|
+
- Concise, functional style
|
|
176
|
+
- Manual parsing (no external parser libraries)
|
|
177
|
+
|
|
178
|
+
### Adding Methods
|
|
179
|
+
|
|
180
|
+
1. Add method name to `METHODS` array
|
|
181
|
+
2. Add case to `applyMethod` switch statement
|
|
182
|
+
3. Propagate `bindings` parameter for nested support
|
|
183
|
+
4. Add tests to `index.test.js`
|
|
184
|
+
5. Update DOCS.md with user-facing documentation
|
|
185
|
+
|
|
186
|
+
### Adding Operators
|
|
187
|
+
|
|
188
|
+
1. Update `tokenizeExpr` regex to include new operator
|
|
189
|
+
2. Add operator precedence in `parseExpr`
|
|
190
|
+
3. Handle in `evalNode` binary operator cases
|
|
191
|
+
4. Add tests
|
|
192
|
+
|
|
193
|
+
### Testing Guidelines
|
|
194
|
+
|
|
195
|
+
- Test each method with basic usage
|
|
196
|
+
- Test chaining combinations
|
|
197
|
+
- Test edge cases (empty arrays, null values)
|
|
198
|
+
- Test error conditions
|
|
199
|
+
- Test nested method scenarios
|
|
200
|
+
- Test all three syntax styles (`$item`, `_`, arrow functions)
|
|
201
|
+
|
|
202
|
+
## Project Structure
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
├── src/
|
|
206
|
+
│ ├── index.js # Main implementation
|
|
207
|
+
│ └── cli.js # CLI tool (if applicable)
|
|
208
|
+
├── test/
|
|
209
|
+
│ └── index.test.js # Test suite
|
|
210
|
+
├── examples/
|
|
211
|
+
│ └── example.js # Usage examples
|
|
212
|
+
├── dist/ # Build outputs (generated)
|
|
213
|
+
├── DOCS.md # User documentation
|
|
214
|
+
├── README.md # Developer documentation (this file)
|
|
215
|
+
├── LICENSE # MIT License
|
|
216
|
+
├── .gitignore
|
|
217
|
+
├── package.json
|
|
218
|
+
├── rollup.config.mjs
|
|
219
|
+
└── jest.config.js
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Design Decisions
|
|
223
|
+
|
|
224
|
+
### Manual Parsing
|
|
225
|
+
|
|
226
|
+
We use a manual tokenizer/parser instead of a library to:
|
|
227
|
+
|
|
228
|
+
- Keep bundle size minimal
|
|
229
|
+
- Avoid external dependencies
|
|
230
|
+
- Maintain full control over syntax
|
|
231
|
+
- Optimize for our specific use case
|
|
232
|
+
|
|
233
|
+
### Three Syntax Styles
|
|
234
|
+
|
|
235
|
+
Supporting `$item`, `_`, and arrow functions provides:
|
|
236
|
+
|
|
237
|
+
- Backward compatibility (`$item`)
|
|
238
|
+
- Conciseness (`_`)
|
|
239
|
+
- Clarity and parent scope access (arrow functions)
|
|
240
|
+
|
|
241
|
+
### No Pipeline Operator
|
|
242
|
+
|
|
243
|
+
The `>` operator was removed to avoid:
|
|
244
|
+
|
|
245
|
+
- Confusion with greater-than comparisons
|
|
246
|
+
- Additional parsing complexity
|
|
247
|
+
- Preference for dot-chaining which is more familiar
|
|
248
|
+
|
|
249
|
+
### Method Chaining Only
|
|
250
|
+
|
|
251
|
+
All operations chain with `.method()` syntax for consistency with JavaScript's native array methods.
|
|
252
|
+
|
|
253
|
+
## Performance Considerations
|
|
254
|
+
|
|
255
|
+
- No AST caching (expressions evaluated fresh each time)
|
|
256
|
+
- Recursive descent parser (adequate for expression complexity)
|
|
257
|
+
- Methods execute eagerly (no lazy evaluation)
|
|
258
|
+
- Suitable for small to medium datasets
|
|
259
|
+
|
|
260
|
+
## Known Limitations
|
|
261
|
+
|
|
262
|
+
- No async support
|
|
263
|
+
- No custom function definitions
|
|
264
|
+
- Single-parameter arrow functions only
|
|
265
|
+
- No array/object literal construction in expressions
|
|
266
|
+
- Limited to JavaScript-compatible JSON data
|
|
267
|
+
|
|
268
|
+
## Release Process
|
|
269
|
+
|
|
270
|
+
1. Update version in `package.json`
|
|
271
|
+
2. Run tests: `npm test`
|
|
272
|
+
3. Build: `npm run build`
|
|
273
|
+
4. Commit changes
|
|
274
|
+
5. Tag release: `git tag v1.0.0`
|
|
275
|
+
6. Push: `git push && git push --tags`
|
|
276
|
+
7. Publish: `npm publish`
|
|
277
|
+
|
|
278
|
+
## License
|
|
279
|
+
|
|
280
|
+
MIT
|
|
281
|
+
query("$.users.map($item.name)", data);
|
|
282
|
+
|
|
283
|
+
// Arithmetic
|
|
284
|
+
query("$.items.map(price * 1.1)", data);
|
|
285
|
+
query("$.items.map(\_.price \* 1.1)", data);
|
|
286
|
+
|
|
287
|
+
// Complex expressions
|
|
288
|
+
query("$.users.map(u => u.age \* 2 + 10)", data);
|
|
289
|
+
|
|
290
|
+
````
|
|
291
|
+
|
|
292
|
+
### filter(condition)
|
|
293
|
+
|
|
294
|
+
Select elements matching a condition.
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
// Comparison operators
|
|
298
|
+
query("$.users.filter(age > 25)", data);
|
|
299
|
+
query('$.users.filter(name == "Alice")', data);
|
|
300
|
+
|
|
301
|
+
// Using underscore
|
|
302
|
+
query("$.users.filter(_.age > 25)", data);
|
|
303
|
+
|
|
304
|
+
// Using arrow syntax
|
|
305
|
+
query("$.users.filter(u => u.age > 25 && u.active)", data);
|
|
306
|
+
|
|
307
|
+
// Logical operators
|
|
308
|
+
query("$.users.filter(age > 25 && active == true)", data);
|
|
309
|
+
query("$.users.filter(age < 20 || age > 60)", data);
|
|
310
|
+
|
|
311
|
+
// Negation
|
|
312
|
+
query("$.users.filter(!active)", data);
|
|
313
|
+
query("$.users.filter(!_.deleted)", data);
|
|
314
|
+
````
|
|
315
|
+
|
|
316
|
+
### sort(expression)
|
|
317
|
+
|
|
318
|
+
Sort array elements. Use negative expression for descending order.
|
|
319
|
+
|
|
320
|
+
```javascript
|
|
321
|
+
// Sort ascending (default)
|
|
322
|
+
query("$.users.sort(age)", data);
|
|
323
|
+
query("$.users.sort(name)", data);
|
|
324
|
+
|
|
325
|
+
// Sort descending (use negative)
|
|
326
|
+
query("$.users.sort(-age)", data);
|
|
327
|
+
query("$.users.sort(-name)", data);
|
|
328
|
+
|
|
329
|
+
// Sort with expressions
|
|
330
|
+
query("$.items.sort(price * quantity)", data);
|
|
331
|
+
query("$.items.sort(-(price * quantity))", data); // Descending
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### join(separator)
|
|
335
|
+
|
|
336
|
+
Join array elements into a string.
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
query('$.tags.join(", ")', data);
|
|
340
|
+
query('$.users.map(name).join(" | ")', data);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### unique(expression)
|
|
344
|
+
|
|
345
|
+
Get unique values from an array.
|
|
346
|
+
|
|
347
|
+
```javascript
|
|
348
|
+
// Unique primitives
|
|
349
|
+
query("$.tags.unique($item)", data);
|
|
350
|
+
|
|
351
|
+
// Unique by property
|
|
352
|
+
query("$.users.unique(department)", data);
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### first()
|
|
356
|
+
|
|
357
|
+
Get the first element.
|
|
358
|
+
|
|
359
|
+
```javascript
|
|
360
|
+
query("$.users.first()", data);
|
|
361
|
+
query("$.users.filter(active).first()", data);
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### last()
|
|
365
|
+
|
|
366
|
+
Get the last element.
|
|
367
|
+
|
|
368
|
+
```javascript
|
|
369
|
+
query("$.users.last()", data);
|
|
370
|
+
query("$.users.sort(age).last()", data);
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### count()
|
|
374
|
+
|
|
375
|
+
Count array elements.
|
|
376
|
+
|
|
377
|
+
```javascript
|
|
378
|
+
query("$.users.count()", data);
|
|
379
|
+
query("$.users.filter(active).count()", data);
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### sum(expression)
|
|
383
|
+
|
|
384
|
+
Sum values in an array.
|
|
385
|
+
|
|
386
|
+
```javascript
|
|
387
|
+
query("$.items.sum(price)", data);
|
|
388
|
+
query("$.numbers.sum($item)", data);
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### avg(expression)
|
|
392
|
+
|
|
393
|
+
Calculate average of values.
|
|
394
|
+
|
|
395
|
+
```javascript
|
|
396
|
+
query("$.users.avg(age)", data);
|
|
397
|
+
query("$.items.avg(price * quantity)", data);
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### min(expression)
|
|
401
|
+
|
|
402
|
+
Find minimum value.
|
|
403
|
+
|
|
404
|
+
```javascript
|
|
405
|
+
query("$.users.min(age)", data);
|
|
406
|
+
query("$.items.min(price)", data);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### max(expression)
|
|
410
|
+
|
|
411
|
+
Find maximum value.
|
|
412
|
+
|
|
413
|
+
```javascript
|
|
414
|
+
query("$.users.max(age)", data);
|
|
415
|
+
query("$.items.max(price)", data);
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Expressions
|
|
419
|
+
|
|
420
|
+
### Variables
|
|
421
|
+
|
|
422
|
+
**Built-in variables:**
|
|
423
|
+
|
|
424
|
+
- `$item` - Current element in iteration
|
|
425
|
+
- `$index` - Current index in iteration
|
|
426
|
+
- `$data` - Root data object
|
|
427
|
+
- `_` - Shorthand for `$item` (concise syntax)
|
|
428
|
+
|
|
429
|
+
**Arrow function syntax:**
|
|
430
|
+
Use arrow functions to name your parameters:
|
|
431
|
+
|
|
432
|
+
```javascript
|
|
433
|
+
// Named parameter (any name you want)
|
|
434
|
+
query("$.users.map(user => user.name)", data);
|
|
435
|
+
query("$.users.filter(person => person.age > 25)", data);
|
|
436
|
+
|
|
437
|
+
// Access parent scope in nested iterations
|
|
438
|
+
query(
|
|
439
|
+
'$.departments.map(dept => dept.employees.map(emp => dept.name + ": " + emp.name))',
|
|
440
|
+
data,
|
|
441
|
+
);
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**Examples:**
|
|
445
|
+
|
|
446
|
+
```javascript
|
|
447
|
+
// Using built-in variables
|
|
448
|
+
query("$.users.map($item.name)", data);
|
|
449
|
+
query("$.users.map($index)", data);
|
|
450
|
+
query("$.users.filter($item.age > $data.minAge)", data);
|
|
451
|
+
|
|
452
|
+
// Using underscore shorthand (more concise)
|
|
453
|
+
query("$.users.map(_.name)", data);
|
|
454
|
+
query("$.items.filter(_.price < 100)", data);
|
|
455
|
+
|
|
456
|
+
// Using arrow syntax for clarity
|
|
457
|
+
query("$.users.map(u => u.name)", data);
|
|
458
|
+
query("$.items.filter(item => item.price < 100)", data);
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**Nested references:**
|
|
462
|
+
|
|
463
|
+
```javascript
|
|
464
|
+
// Underscore shadows in nested contexts
|
|
465
|
+
query("$.departments.map(_.employees.map(_.name))", data);
|
|
466
|
+
// Outer _ = department, inner _ = employee (shadowed)
|
|
467
|
+
|
|
468
|
+
// Use arrow syntax when you need parent access
|
|
469
|
+
query(
|
|
470
|
+
'$.departments.map(dept => dept.employees.map(emp => dept.name + ": " + emp.name))',
|
|
471
|
+
data,
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
// Mix both styles
|
|
475
|
+
query("$.groups.map(g => g.items.filter(_.value > 10).map(_.name))", data);
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Operators
|
|
479
|
+
|
|
480
|
+
**Arithmetic**: `+`, `-`, `*`, `/`, `%`
|
|
481
|
+
|
|
482
|
+
```javascript
|
|
483
|
+
query("$.items.map(price * 1.2)", data);
|
|
484
|
+
query("$.users.filter(age % 2 == 0)", data);
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Comparison**: `==`, `!=`, `<`, `>`, `<=`, `>=`
|
|
488
|
+
|
|
489
|
+
```javascript
|
|
490
|
+
query("$.users.filter(age >= 18)", data);
|
|
491
|
+
query("$.items.filter(stock != 0)", data);
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Logical**: `&&`, `||`, `!`
|
|
495
|
+
|
|
496
|
+
```javascript
|
|
497
|
+
query("$.users.filter(age > 18 && active == true)", data);
|
|
498
|
+
query("$.items.filter(inStock || onOrder)", data);
|
|
499
|
+
query("$.users.filter(!deleted)", data);
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Literals
|
|
503
|
+
|
|
504
|
+
- **Numbers**: `42`, `3.14`, `-10`, `1.5e10`
|
|
505
|
+
- **Strings**: `"hello"`, `'world'`
|
|
506
|
+
- **Booleans**: `true`, `false`
|
|
507
|
+
- **Null**: `null`
|
|
508
|
+
- **Undefined**: `undefined`
|
|
509
|
+
|
|
510
|
+
```javascript
|
|
511
|
+
query("$.users.filter(age > 25)", data);
|
|
512
|
+
query('$.users.filter(name == "Alice")', data);
|
|
513
|
+
query("$.users.filter(deleted == null)", data);
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Dates
|
|
517
|
+
|
|
518
|
+
Use `@` prefix for ISO date strings:
|
|
519
|
+
|
|
520
|
+
```javascript
|
|
521
|
+
const data = [
|
|
522
|
+
{ event: "Launch", date: "2024-01-15" },
|
|
523
|
+
{ event: "Update", date: "2024-06-20" },
|
|
524
|
+
];
|
|
525
|
+
|
|
526
|
+
query("filter(date > @2024-03-01).map(event)", data);
|
|
527
|
+
// => ['Update']
|
|
528
|
+
|
|
529
|
+
// With timestamps
|
|
530
|
+
query("filter(createdAt > @2024-01-01T12:00:00Z)", data);
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Property Access
|
|
534
|
+
|
|
535
|
+
Access nested properties in expressions:
|
|
536
|
+
|
|
537
|
+
```javascript
|
|
538
|
+
query("$.users.map($item.address.city)", data);
|
|
539
|
+
query("$.users.map(_.address.city)", data);
|
|
540
|
+
query("$.users.map(u => u.address.city)", data);
|
|
541
|
+
query("$.items.filter($item.specs.weight > 100)", data);
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Array Access
|
|
545
|
+
|
|
546
|
+
Use brackets in expressions:
|
|
547
|
+
|
|
548
|
+
```javascript
|
|
549
|
+
query("$.users.map($item.tags[0])", data);
|
|
550
|
+
query("$.users.map(_.tags[0])", data);
|
|
551
|
+
query("$.data.map($item.values[$index])", data);
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Nested Method Calls
|
|
555
|
+
|
|
556
|
+
Call methods within expressions for complex transformations:
|
|
557
|
+
|
|
558
|
+
```javascript
|
|
559
|
+
const data = {
|
|
560
|
+
departments: [
|
|
561
|
+
{
|
|
562
|
+
name: "Engineering",
|
|
563
|
+
employees: [
|
|
564
|
+
{ name: "Alice", age: 30 },
|
|
565
|
+
{ name: "Bob", age: 25 },
|
|
566
|
+
{ name: "Carol", age: 35 },
|
|
567
|
+
],
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: "Sales",
|
|
571
|
+
employees: [
|
|
572
|
+
{ name: "Dave", age: 40 },
|
|
573
|
+
{ name: "Eve", age: 28 },
|
|
574
|
+
],
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// Filter and map employees within each department (concise)
|
|
580
|
+
query("$.departments.map(_.employees.filter(age > 28).map(name))", data);
|
|
581
|
+
// => [['Alice', 'Carol'], ['Dave']]
|
|
582
|
+
|
|
583
|
+
// With arrow syntax for parent access
|
|
584
|
+
query(
|
|
585
|
+
'$.departments.map(dept => dept.employees.map(emp => dept.name + ": " + emp.name))',
|
|
586
|
+
data,
|
|
587
|
+
);
|
|
588
|
+
// => [['Engineering: Alice', 'Engineering: Bob', 'Engineering: Carol'], ['Sales: Dave', 'Sales: Eve']]
|
|
589
|
+
|
|
590
|
+
// Count employees per department
|
|
591
|
+
query("$.departments.map(_.employees.count())", data);
|
|
592
|
+
// => [3, 2]
|
|
593
|
+
|
|
594
|
+
// Average age per department
|
|
595
|
+
query("$.departments.map(_.employees.avg(age))", data);
|
|
596
|
+
// => [30, 34]
|
|
597
|
+
|
|
598
|
+
// Multiple levels of nesting with underscore
|
|
599
|
+
query("$.departments.map(_.employees.sort(age).first().name)", data);
|
|
600
|
+
// => ['Bob', 'Eve']
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
## Examples
|
|
604
|
+
|
|
605
|
+
### E-commerce
|
|
606
|
+
|
|
607
|
+
```javascript
|
|
608
|
+
const orders = {
|
|
609
|
+
items: [
|
|
610
|
+
{ name: "Laptop", price: 999, quantity: 2, inStock: true },
|
|
611
|
+
{ name: "Mouse", price: 25, quantity: 5, inStock: true },
|
|
612
|
+
{ name: "Keyboard", price: 75, quantity: 0, inStock: false },
|
|
613
|
+
],
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// Total value of in-stock items
|
|
617
|
+
query("$.items.filter(inStock).map(price * quantity).sum($item)", orders);
|
|
618
|
+
// => 2048
|
|
619
|
+
|
|
620
|
+
// Most expensive item name
|
|
621
|
+
query("$.items.sort(price).last().name", orders);
|
|
622
|
+
// => 'Laptop'
|
|
623
|
+
|
|
624
|
+
// Average price
|
|
625
|
+
query("$.items.avg(price)", orders);
|
|
626
|
+
// => 366.33...
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### User Management
|
|
630
|
+
|
|
631
|
+
```javascript
|
|
632
|
+
const company = {
|
|
633
|
+
departments: [
|
|
634
|
+
{
|
|
635
|
+
name: "Engineering",
|
|
636
|
+
employees: [
|
|
637
|
+
{ name: "Alice", salary: 120000, startDate: "2020-01-15" },
|
|
638
|
+
{ name: "Bob", salary: 95000, startDate: "2021-06-01" },
|
|
639
|
+
],
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
name: "Sales",
|
|
643
|
+
employees: [{ name: "Carol", salary: 85000, startDate: "2019-03-10" }],
|
|
644
|
+
},
|
|
645
|
+
],
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// All department names
|
|
649
|
+
query('$.departments.map(name).join(", ")', company);
|
|
650
|
+
// => 'Engineering, Sales'
|
|
651
|
+
|
|
652
|
+
// High earners in each department
|
|
653
|
+
query(
|
|
654
|
+
"$.departments.map($item.employees.filter(salary > 100000).map(name))",
|
|
655
|
+
company,
|
|
656
|
+
);
|
|
657
|
+
// => [['Alice'], []]
|
|
658
|
+
|
|
659
|
+
// Recent hires
|
|
660
|
+
query(
|
|
661
|
+
"$.departments[0].employees.filter(startDate > @2021-01-01).map(name)",
|
|
662
|
+
company,
|
|
663
|
+
);
|
|
664
|
+
// => ['Bob']
|
|
665
|
+
|
|
666
|
+
// Department sizes
|
|
667
|
+
query("$.departments.map($item.employees.count())", company);
|
|
668
|
+
// => [2, 1]
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### Data Analysis
|
|
672
|
+
|
|
673
|
+
```javascript
|
|
674
|
+
const metrics = {
|
|
675
|
+
daily: [
|
|
676
|
+
{ date: "2024-01-01", views: 1200, clicks: 45 },
|
|
677
|
+
{ date: "2024-01-02", views: 1500, clicks: 67 },
|
|
678
|
+
{ date: "2024-01-03", views: 980, clicks: 32 },
|
|
679
|
+
],
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// Click-through rates
|
|
683
|
+
query("$.daily.map(clicks / views * 100)", metrics);
|
|
684
|
+
// => [3.75, 4.47, 3.27]
|
|
685
|
+
|
|
686
|
+
// Best performing day
|
|
687
|
+
query("$.daily.sort(clicks).last().date", metrics);
|
|
688
|
+
// => '2024-01-02'
|
|
689
|
+
|
|
690
|
+
// Total engagement
|
|
691
|
+
query("$.daily.sum(views + clicks)", metrics);
|
|
692
|
+
// => 3824
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
## API Reference
|
|
696
|
+
|
|
697
|
+
### query(expression, data)
|
|
698
|
+
|
|
699
|
+
Execute a query against data.
|
|
700
|
+
|
|
701
|
+
**Parameters:**
|
|
702
|
+
|
|
703
|
+
- `expression` (string): Query expression
|
|
704
|
+
- `data` (any): Data to query
|
|
705
|
+
|
|
706
|
+
**Returns:** Query result
|
|
707
|
+
|
|
708
|
+
```javascript
|
|
709
|
+
const result = query("$.users.filter(age > 25)", data);
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### tokenize(expression)
|
|
713
|
+
|
|
714
|
+
Parse expression into tokens (exported for testing/debugging).
|
|
715
|
+
|
|
716
|
+
**Parameters:**
|
|
717
|
+
|
|
718
|
+
- `expression` (string): Query expression
|
|
719
|
+
|
|
720
|
+
**Returns:** Array of tokens
|
|
721
|
+
|
|
722
|
+
```javascript
|
|
723
|
+
import { tokenize } from "jsonq";
|
|
724
|
+
const tokens = tokenize("$.users.map(name)");
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### evaluate(tokens, data)
|
|
728
|
+
|
|
729
|
+
Evaluate parsed tokens against data (exported for testing/debugging).
|
|
730
|
+
|
|
731
|
+
**Parameters:**
|
|
732
|
+
|
|
733
|
+
- `tokens` (Array): Parsed tokens
|
|
734
|
+
- `data` (any): Data to evaluate
|
|
735
|
+
|
|
736
|
+
**Returns:** Evaluation result
|
|
737
|
+
|
|
738
|
+
```javascript
|
|
739
|
+
import { evaluate, tokenize } from "jsonq";
|
|
740
|
+
const tokens = tokenize("$.users.count()");
|
|
741
|
+
const result = evaluate(tokens, data);
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
## Advanced Usage
|
|
745
|
+
|
|
746
|
+
### Complex Filters
|
|
747
|
+
|
|
748
|
+
Combine multiple conditions:
|
|
749
|
+
|
|
750
|
+
```javascript
|
|
751
|
+
query(
|
|
752
|
+
`
|
|
753
|
+
$.users
|
|
754
|
+
.filter(age >= 21 && age <= 65)
|
|
755
|
+
.filter(active == true)
|
|
756
|
+
.filter(verified == true)
|
|
757
|
+
.sort(name)
|
|
758
|
+
`,
|
|
759
|
+
data,
|
|
760
|
+
);
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
### Nested Access Patterns
|
|
764
|
+
|
|
765
|
+
Access deeply nested data:
|
|
766
|
+
|
|
767
|
+
```javascript
|
|
768
|
+
query('$.company.departments[0].teams[2].members.filter(role == "lead")', data);
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
Combine nested methods with property navigation:
|
|
772
|
+
|
|
773
|
+
```javascript
|
|
774
|
+
// Get top performer from each department
|
|
775
|
+
query("$.departments.map($item.employees.sort(performance).last())", data);
|
|
776
|
+
|
|
777
|
+
// Filter departments by employee criteria
|
|
778
|
+
query(
|
|
779
|
+
"$.departments.filter($item.employees.filter(certified == true).count() > 5)",
|
|
780
|
+
data,
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
// Aggregate across multiple levels
|
|
784
|
+
query("$.regions.map($item.stores.map($item.sales.sum(amount)))", data);
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### Dynamic Calculations
|
|
788
|
+
|
|
789
|
+
Use expressions in any method:
|
|
790
|
+
|
|
791
|
+
```javascript
|
|
792
|
+
query("$.products.filter((price - cost) / price > 0.3)", data);
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
### Index-Based Operations
|
|
796
|
+
|
|
797
|
+
Use `$index` for position-dependent logic:
|
|
798
|
+
|
|
799
|
+
```javascript
|
|
800
|
+
query("$.items.filter($index % 2 == 0)", data); // Even indices
|
|
801
|
+
query("$.items.map($index * 10 + $item.value)", data);
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
## Type Handling
|
|
805
|
+
|
|
806
|
+
### Null and Undefined
|
|
807
|
+
|
|
808
|
+
Safe navigation with optional chaining behavior:
|
|
809
|
+
|
|
810
|
+
```javascript
|
|
811
|
+
const data = [{ user: { name: "Alice" } }, { user: null }, {}];
|
|
812
|
+
|
|
813
|
+
query("map($item.user.name)", data);
|
|
814
|
+
// => ['Alice', undefined, undefined]
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
### Missing Properties
|
|
818
|
+
|
|
819
|
+
Accessing non-existent properties returns `undefined`:
|
|
820
|
+
|
|
821
|
+
```javascript
|
|
822
|
+
query("$.users.map(nonexistent)", data);
|
|
823
|
+
// => [undefined, undefined, ...]
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### Type Coercion
|
|
827
|
+
|
|
828
|
+
Dates are automatically coerced for comparison:
|
|
829
|
+
|
|
830
|
+
```javascript
|
|
831
|
+
query("filter(dateString > @2024-01-01)", data);
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
## Performance Tips
|
|
835
|
+
|
|
836
|
+
1. **Filter Early**: Apply filters before expensive operations
|
|
837
|
+
|
|
838
|
+
```javascript
|
|
839
|
+
// Good
|
|
840
|
+
query("$.users.filter(active).map(expensiveOperation)", data);
|
|
841
|
+
|
|
842
|
+
// Less efficient
|
|
843
|
+
query("$.users.map(expensiveOperation).filter(active)", data);
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
2. **Avoid Redundant Sorts**: Sort only when necessary
|
|
847
|
+
|
|
848
|
+
```javascript
|
|
849
|
+
// If you only need first/last, consider avoiding sort
|
|
850
|
+
query("$.users.sort(age).first()", data);
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
3. **Use Specific Expressions**: More specific filters run faster
|
|
854
|
+
|
|
855
|
+
```javascript
|
|
856
|
+
// More specific
|
|
857
|
+
query("$.users.filter(id == 123)", data);
|
|
858
|
+
|
|
859
|
+
// Less specific
|
|
860
|
+
query("$.users.filter(id > 0)", data);
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
## Error Handling
|
|
864
|
+
|
|
865
|
+
The library throws errors for:
|
|
866
|
+
|
|
867
|
+
- Unknown methods
|
|
868
|
+
- Invalid expressions
|
|
869
|
+
- Unknown variables
|
|
870
|
+
- Unexpected tokens
|
|
871
|
+
- Invalid literals
|
|
872
|
+
|
|
873
|
+
```javascript
|
|
874
|
+
try {
|
|
875
|
+
query("$.users.unknownMethod()", data);
|
|
876
|
+
} catch (error) {
|
|
877
|
+
console.error(error.message); // "Unknown method: unknownMethod"
|
|
878
|
+
}
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
## Testing
|
|
882
|
+
|
|
883
|
+
Run the test suite:
|
|
884
|
+
|
|
885
|
+
```bash
|
|
886
|
+
npm test
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
## License
|
|
890
|
+
|
|
891
|
+
MIT
|