/ src / pipeline / steps / transform.ts
transform.ts
 1  /**
 2   * Pipeline steps: data transforms — select, map, filter, sort, limit.
 3   */
 4  
 5  import type { IPage } from '../../types.js';
 6  import { render, evalExpr } from '../template.js';
 7  
 8  import { isRecord } from '../../utils.js';
 9  
10  export async function stepSelect(_page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
11    const pathStr = String(render(params, { args, data }));
12    if (data && typeof data === 'object') {
13      let current: unknown = data;
14      for (const part of pathStr.split('.')) {
15        if (isRecord(current)) current = current[part];
16        else if (Array.isArray(current) && /^\d+$/.test(part)) current = current[parseInt(part, 10)];
17        else return null;
18      }
19      return current;
20    }
21    return data;
22  }
23  
24  export async function stepMap(_page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
25    if (!data || typeof data !== 'object') return data;
26    let source: unknown = data;
27  
28    // Support inline select: { map: { select: 'path', key: '${{ item.x }}' } }
29    if (isRecord(params) && 'select' in params) {
30      source = await stepSelect(null, params.select, data, args);
31    }
32  
33    if (!source || typeof source !== 'object') return source;
34  
35    let items: unknown[] = Array.isArray(source) ? source : [source];
36    if (isRecord(source) && Array.isArray(source.data)) items = source.data;
37    const result: Array<Record<string, unknown>> = [];
38    const templateParams = isRecord(params) ? params : {};
39    for (let i = 0; i < items.length; i++) {
40      const item = items[i];
41      const row: Record<string, unknown> = {};
42      for (const [key, template] of Object.entries(templateParams)) {
43        if (key === 'select') continue;
44        row[key] = render(template, { args, data: source, root: data, item, index: i });
45      }
46      result.push(row);
47    }
48    return result;
49  }
50  
51  export async function stepFilter(_page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
52    if (!Array.isArray(data)) return data;
53    return data.filter((item, i) => evalExpr(String(params), { args, item, index: i }));
54  }
55  
56  export async function stepSort(_page: IPage | null, params: unknown, data: unknown, _args: Record<string, unknown>): Promise<unknown> {
57    if (!Array.isArray(data)) return data;
58    const key = isRecord(params) ? String(params.by ?? '') : String(params);
59    const reverse = isRecord(params) ? params.order === 'desc' : false;
60    return [...data].sort((a, b) => {
61      const left = isRecord(a) ? a[key] : undefined;
62      const right = isRecord(b) ? b[key] : undefined;
63      const cmp = String(left ?? '').localeCompare(String(right ?? ''), undefined, { numeric: true });
64      return reverse ? -cmp : cmp;
65    });
66  }
67  
68  export async function stepLimit(_page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
69    if (!Array.isArray(data)) return data;
70    return data.slice(0, Number(render(params, { args, data })));
71  }