mirror of
https://github.com/rooyca/query-json.git
synced 2026-07-22 10:10:29 +00:00
feat(#15): multi index & filtering
This commit is contained in:
parent
fe45c346ec
commit
19cd998721
5 changed files with 156 additions and 20 deletions
22
README.md
22
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
146
src/functions.ts
146
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<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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue