feat(#15): multi index & filtering

This commit is contained in:
rooyca 2026-05-19 17:07:20 -05:00
parent fe45c346ec
commit 19cd998721
5 changed files with 156 additions and 20 deletions

View file

@ -111,6 +111,21 @@ Or query a specific property across all array elements:
@data.json>characters[*].name;
```
### Array Index Selection
Select specific indices from an array:
```
@data.json>characters[0,3].name;
@data.json>characters.{0,3}.name;
```
Select a range using slice notation:
```
@data.json>characters[0:3].name;
```
### Property Substitution
Use frontmatter properties in your queries with `{this.PROPERTY}`:
@ -144,6 +159,12 @@ Filter array elements using bracket notation with conditions:
```
~~~
Nested keys are supported in conditions, and a leading `.` is allowed when filtering a top-level array:
```
@data.json>[.show.ids.trakt == 1 || .show.ids.trakt == 9].show;
```
**Supported operators:**
**Logical:**
@ -435,4 +456,3 @@ Contributions are welcome! If you encounter bugs or have feature suggestions:
- Created by [rooyca](https://github.com/rooyca)
- Table formatting improvements by [@ozppupbg](https://github.com/ozppupbg)

View file

@ -1,7 +1,7 @@
{
"id": "query-json",
"name": "Query JSON",
"version": "0.1.8",
"version": "0.1.9",
"minAppVersion": "0.15.0",
"description": "Read, query and work with JSON.",
"author": "rooyca",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "query-json",
"version": "0.1.8",
"version": "0.1.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "query-json",
"version": "0.1.7",
"version": "0.1.9",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",

View file

@ -1,6 +1,6 @@
{
"name": "query-json",
"version": "0.1.8",
"version": "0.1.9",
"description": "Read, query and work with JSON.",
"main": "main.js",
"scripts": {

View file

@ -8,6 +8,8 @@ type JsonArray = JsonValue[];
export type QueryPart =
| { type: 'field'; name: string }
| { type: 'index'; index: number }
| { type: 'indexList'; field: string | null; indices: number[] }
| { type: 'slice'; field: string | null; start: number | null; end: number | null }
| { type: 'multiField'; fields: string[] }
| { type: 'filter'; field: string | null; condition: string };
@ -56,6 +58,25 @@ function splitByTopLevelDot(query: string): string[] {
return parts.filter((part) => part.length > 0);
}
function parseIndexList(content: string): number[] | null {
const trimmed = content.trim();
if (!/^\d+(?:\s*,\s*\d+)*$/.test(trimmed)) {
return null;
}
return trimmed.split(',').map((part) => Number.parseInt(part.trim(), 10));
}
function parseSlice(content: string): { start: number | null; end: number | null } | null {
const trimmed = content.trim();
const match = trimmed.match(/^(\d*)\s*:\s*(\d*)$/);
if (!match) {
return null;
}
const start = match[1] === '' ? null : Number.parseInt(match[1], 10);
const end = match[2] === '' ? null : Number.parseInt(match[2], 10);
return { start, end };
}
export function parseQuery(query: string): QueryPart[] {
const parts = splitByTopLevelDot(query.trim());
const parsed = parts.map((part): QueryPart => {
@ -73,10 +94,29 @@ export function parseQuery(query: string): QueryPart[] {
const filterMatch = part.match(/^(\w*)\[(.*)\]$/);
if (filterMatch) {
const field = filterMatch[1] || null;
const condition = filterMatch[2].trim();
const indexList = parseIndexList(condition);
if (indexList) {
return {
type: 'indexList',
field,
indices: indexList,
};
}
const slice = parseSlice(condition);
if (slice) {
return {
type: 'slice',
field,
start: slice.start,
end: slice.end,
};
}
return {
type: 'filter',
field: filterMatch[1] || null,
condition: filterMatch[2].trim(),
field,
condition,
};
}
@ -199,6 +239,23 @@ function parseLiteral(value: string): JsonValue {
return trimmed;
}
function applyIndexList(array: unknown[], indices: number[]): unknown[] {
const selected: unknown[] = [];
for (const index of indices) {
if (!Number.isInteger(index) || index < 0 || index >= array.length) {
continue;
}
selected.push(array[index]);
}
return selected;
}
function applySlice(array: unknown[], start: number | null, end: number | null): unknown[] {
const from = start ?? 0;
const to = end ?? array.length;
return array.slice(from, to);
}
export function executeQuery(json: unknown, parsedQuery: QueryPart[]): unknown {
let result: unknown = json;
@ -228,21 +285,57 @@ export function executeQuery(json: unknown, parsedQuery: QueryPart[]): unknown {
continue;
}
if (part.type === 'indexList') {
let arrayToIndex: unknown = result;
if (part.field) {
if (!result || typeof result !== 'object' || Array.isArray(result)) {
return undefined;
}
arrayToIndex = (result as JsonObject)[part.field];
}
if (!Array.isArray(arrayToIndex)) {
return undefined;
}
result = applyIndexList(arrayToIndex, part.indices);
continue;
}
if (part.type === 'slice') {
let arrayToSlice: unknown = result;
if (part.field) {
if (!result || typeof result !== 'object' || Array.isArray(result)) {
return undefined;
}
arrayToSlice = (result as JsonObject)[part.field];
}
if (!Array.isArray(arrayToSlice)) {
return undefined;
}
result = applySlice(arrayToSlice, part.start, part.end);
continue;
}
if (part.type === 'multiField') {
if (Array.isArray(result)) {
result = result.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
return {};
}
const source = item as JsonObject;
const extracted: JsonObject = {};
for (const field of part.fields) {
if (Object.prototype.hasOwnProperty.call(source, field)) {
extracted[field] = source[field];
const allNumeric = part.fields.every((field) => /^\d+$/.test(field));
if (allNumeric) {
const indices = part.fields.map((field) => Number.parseInt(field, 10));
result = applyIndexList(result, indices);
} else {
result = result.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
return {};
}
}
return extracted;
});
const source = item as JsonObject;
const extracted: JsonObject = {};
for (const field of part.fields) {
if (Object.prototype.hasOwnProperty.call(source, field)) {
extracted[field] = source[field];
}
}
return extracted;
});
}
} else if (result && typeof result === 'object' && !Array.isArray(result)) {
const source = result as JsonObject;
const extracted: JsonObject = {};
@ -277,6 +370,29 @@ export function executeQuery(json: unknown, parsedQuery: QueryPart[]): unknown {
return result;
}
function getValueByPath(obj: JsonObject, path: string): unknown {
const cleaned = path.startsWith('.') ? path.slice(1) : path;
if (cleaned === '') {
return undefined;
}
return cleaned.split('.').reduce<unknown>((acc, key) => {
if (acc === null || acc === undefined) {
return undefined;
}
if (Array.isArray(acc)) {
if (!/^\d+$/.test(key)) {
return undefined;
}
const index = Number.parseInt(key, 10);
return acc[index];
}
if (typeof acc !== 'object') {
return undefined;
}
return (acc as JsonObject)[key];
}, obj);
}
function evaluateConditions(item: unknown, conditions: Condition[], operators: LogicalOperator[]): boolean {
if (conditions.length === 0) return true;
let result = evaluateCondition(item, conditions[0]);
@ -296,7 +412,7 @@ function evaluateCondition(item: unknown, condition: Condition): boolean {
}
const obj = item as JsonObject;
const itemValue = obj[condition.key];
const itemValue = getValueByPath(obj, condition.key);
const queryValue = parseLiteral(condition.value);
switch (condition.operator) {