feat(webapp): support .length in smart-column JSON paths
A dot-accessed .length now resolves consistently to an array or string length, or an object's key count, so a column can show e.g. the number of items. A bracket-quoted ['length'] still reads a real property named length.
This commit is contained in:
@@ -177,7 +177,8 @@ export function AddSmartColumnDialog({
|
||||
/>
|
||||
<Paragraph variant="extra-small" className="text-text-dimmed">
|
||||
Dot and bracket notation, e.g. <code>$.order.total</code> or{" "}
|
||||
<code>$.items[0].sku</code>.
|
||||
<code>$.items[0].sku</code>. Use <code>.length</code> for an array, string, or
|
||||
key count.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
|
||||
@@ -93,6 +93,19 @@ describe("getAtPath", () => {
|
||||
expect(getAtPath(obj, "$.a..b")).toBeUndefined();
|
||||
expect(getAtPath(obj, "$.a[b]")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("computes a dot-accessed .length for arrays, strings, and objects", () => {
|
||||
const data = { tags: ["a", "b", "c"], name: "hello", info: { x: 1, y: 2 }, count: 5 };
|
||||
expect(getAtPath(data, "$.tags.length")).toBe(3);
|
||||
expect(getAtPath(data, "$.name.length")).toBe(5);
|
||||
expect(getAtPath(data, "$.info.length")).toBe(2);
|
||||
expect(getAtPath(data, "$.count.length")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("treats a bracket-quoted ['length'] as a literal key, not the computed length", () => {
|
||||
expect(getAtPath({ length: 42 }, "$['length']")).toBe(42);
|
||||
expect(getAtPath({ length: 42 }, "$.length")).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractSmartValue", () => {
|
||||
|
||||
@@ -57,11 +57,20 @@ export function extractSmartValue(parsed: ParsedSource, path: string): SmartCell
|
||||
|
||||
const PATH_TOKEN_RE = /\.([^.[\]]+)|\[(\d+)\]|\['([^']*)'\]|\["([^"]*)"\]/g;
|
||||
|
||||
type PathToken =
|
||||
| { kind: "dot"; key: string }
|
||||
| { kind: "key"; key: string }
|
||||
| { kind: "index"; index: number };
|
||||
|
||||
/**
|
||||
* Read a value out of a parsed object with dot/bracket notation. Accepts a
|
||||
* leading `$`, dotted keys, and numeric or quoted bracket indices, e.g.
|
||||
* `$.failed`, `suites[0].name`, `$['a.b'].c`. Returns undefined when any
|
||||
* segment is missing.
|
||||
*
|
||||
* A dot-accessed `.length` is computed: array/string length, or an object's
|
||||
* key count. To read a real property literally named `length`, use a bracket
|
||||
* key (`['length']`).
|
||||
*/
|
||||
export function getAtPath(root: unknown, path: string): unknown {
|
||||
let normalized = path.trim();
|
||||
@@ -71,7 +80,7 @@ export function getAtPath(root: unknown, path: string): unknown {
|
||||
normalized = `.${normalized}`;
|
||||
}
|
||||
|
||||
const tokens: (string | number)[] = [];
|
||||
const tokens: PathToken[] = [];
|
||||
let lastIndex = 0;
|
||||
PATH_TOKEN_RE.lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
@@ -79,18 +88,31 @@ export function getAtPath(root: unknown, path: string): unknown {
|
||||
if (match.index !== lastIndex) return undefined;
|
||||
lastIndex = PATH_TOKEN_RE.lastIndex;
|
||||
|
||||
if (match[1] !== undefined) tokens.push(match[1]);
|
||||
else if (match[2] !== undefined) tokens.push(Number(match[2]));
|
||||
else if (match[3] !== undefined) tokens.push(match[3]);
|
||||
else if (match[4] !== undefined) tokens.push(match[4]);
|
||||
if (match[1] !== undefined) tokens.push({ kind: "dot", key: match[1] });
|
||||
else if (match[2] !== undefined) tokens.push({ kind: "index", index: Number(match[2]) });
|
||||
else if (match[3] !== undefined) tokens.push({ kind: "key", key: match[3] });
|
||||
else if (match[4] !== undefined) tokens.push({ kind: "key", key: match[4] });
|
||||
}
|
||||
if (lastIndex !== normalized.length) return undefined;
|
||||
|
||||
let current: unknown = root;
|
||||
for (const token of tokens) {
|
||||
if (current === null || current === undefined) return undefined;
|
||||
|
||||
if (token.kind === "dot" && token.key === "length") {
|
||||
if (Array.isArray(current) || typeof current === "string") {
|
||||
current = current.length;
|
||||
} else if (typeof current === "object") {
|
||||
current = Object.keys(current).length;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof current !== "object") return undefined;
|
||||
current = (current as Record<string | number, unknown>)[token];
|
||||
const key = token.kind === "index" ? token.index : token.key;
|
||||
current = (current as Record<string | number, unknown>)[key];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user