From 19cd9987212d2dbbf6761654c3ab68b438bd08fb Mon Sep 17 00:00:00 2001 From: rooyca Date: Tue, 19 May 2026 17:07:20 -0500 Subject: [PATCH] feat(#15): multi index & filtering --- README.md | 22 ++++++- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/functions.ts | 146 +++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 156 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index eddb864..824d18c 100644 --- a/README.md +++ b/README.md @@ -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) - diff --git a/manifest.json b/manifest.json index c94d28e..b1dfe9f 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/package-lock.json b/package-lock.json index 1dda8ac..daf72ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 7d55973..0f63f30 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/functions.ts b/src/functions.ts index dd1843b..e68d42c 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -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((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) {