Jaspers Hub

rym / excel-export

pluginReviewed★ 0↓ 0

Excel Export: Builds .xlsx workbooks from supplied tables or grid panel data and writes/opens them in the plugin's exports folder.

3.9 KB
1import type { BackendContext } from '@jaspers-ai/sdk'
2import type { TableInput } from './save'
3
4type Rec = Record<string, unknown>
5
6async function tryGet(ctx: BackendContext, path: string): Promise<unknown> {
7 try {
8 return await ctx.state.get(path)
9 } catch {
10 return null
11 }
12}
13
14const isRec = (v: unknown): v is Rec => !!v && typeof v === 'object' && !Array.isArray(v)
15
16function columnName(c: unknown): string {
17 if (typeof c === 'string') return c
18 if (isRec(c)) return String(c.label ?? c.title ?? c.header ?? c.name ?? c.key ?? c.field ?? '')
19 return String(c)
20}
21
22function columnKey(c: unknown): string {
23 if (isRec(c)) return String(c.key ?? c.field ?? c.name ?? c.id ?? columnName(c))
24 return String(c)
25}
26
27/** A table from an object with rows under a usual key, and columns if it names them. */
28function fromObject(o: unknown): { columns: string[]; rows: unknown[] } | null {
29 if (!isRec(o)) return null
30 for (const key of ['rows', 'data', 'items', 'results', 'records', 'table']) {
31 const v = o[key]
32 if (isRec(v)) {
33 const inner = fromObject(v)
34 if (inner) return { columns: inner.columns, rows: inner.rows }
35 }
36 if (!Array.isArray(v) || v.length === 0) continue
37 const cols = (o.columns ?? o.headers ?? o.fields) as unknown[] | undefined
38 if (Array.isArray(v[0])) {
39 const columns = Array.isArray(cols) ? cols.map(columnName) : v[0].map((_, i) => `Column ${i + 1}`)
40 return { columns, rows: v as unknown[] }
41 }
42 if (isRec(v[0])) {
43 const keys = Array.isArray(cols) ? cols.map(columnKey) : Object.keys(v[0])
44 const names = Array.isArray(cols) ? cols.map(columnName) : keys
45 return { columns: names, rows: (v as Rec[]).map((r) => keys.map((k) => r[k])) }
46 }
47 }
48 return null
49}
50
51/** A table from text: a Markdown pipe table, or tab-separated lines. */
52function fromText(text: unknown): { columns: string[]; rows: unknown[] } | null {
53 if (typeof text !== 'string') return null
54 const lines = text.split(/\r?\n/)
55 const pipe = lines.filter((l) => /^\s*\|.*\|\s*$/.test(l))
56 if (pipe.length >= 2) {
57 const cells = (l: string) => l.trim().replace(/^\||\|$/g, '').split('|').map((c) => c.trim())
58 const body = pipe.filter((l) => !/^\s*\|[\s:|-]+\|\s*$/.test(l))
59 if (body.length >= 1) return { columns: cells(body[0]!), rows: body.slice(1).map(cells) }
60 }
61 const tsv = lines.filter((l) => l.includes('\t'))
62 if (tsv.length >= 2) return { columns: tsv[0]!.split('\t'), rows: tsv.slice(1).map((l) => l.split('\t')) }
63 return null
64}
65
66/** A panel's table: its rows from the panel's output, text, or state, with a title. */
67export async function readPanelTable(ctx: BackendContext, id: string, title?: string): Promise<TableInput> {
68 const panel = await tryGet(ctx, `panels/${id}`)
69 const state = (await tryGet(ctx, `panels/${id}/state`)) ?? (isRec(panel) ? panel.state : null)
70 const output = (await tryGet(ctx, `panels/${id}/output`)) ?? (isRec(panel) ? panel.output : null)
71 const text = (await tryGet(ctx, `panels/${id}/text`)) ?? (isRec(panel) ? panel.text : null)
72 if (panel === null && state === null && output === null) throw new Error(`No panel ${id} on the grid.`)
73 const table = fromObject(output) ?? fromObject(state) ?? fromText(text) ?? fromObject(panel)
74 const named =
75 title ??
76 [state, output, panel].map((o) => (isRec(o) ? (o.title ?? o.name) : undefined)).find((t) => typeof t === 'string') ??
77 `Panel ${id}`
78 if (!table) {
79 const s = isRec(state) ? state : {}
80 const how = s.sql
81 ? `its SQL (${String(s.sql).slice(0, 200)})`
82 : s.source
83 ? `source ${String(s.source)} with args ${JSON.stringify(s.args ?? {}).slice(0, 200)}`
84 : 'its source'
85 throw new Error(
86 `Panel ${id} publishes no rows this plugin can read. Run ${how}, then pass the rows to excel-export/export-tables. Panel state: ${JSON.stringify(state).slice(0, 300)}; output keys: ${isRec(output) ? Object.keys(output).join(', ') : String(output)}`,
87 )
88 }
89 return { title: String(named), columns: table.columns, rows: table.rows }
90}