-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathexport-format.ts
More file actions
199 lines (181 loc) · 7.57 KB
/
Copy pathexport-format.ts
File metadata and controls
199 lines (181 loc) · 7.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
/**
* Type-aware value formatting for the streaming data export route
* (`GET /data/:object/export`).
*
* The raw rows returned by `findData` carry *storage* values: lookup / user
* fields hold ids (or, when `$expand`-ed, nested records), select fields hold
* option codes, booleans hold true/false, dates hold ISO strings. None of those
* read well in a spreadsheet. These helpers turn each value into a human
* readable cell using the object's field metadata.
*
* Contract: when no field metadata is available (schema lookup failed or carried
* no fields) every helper is a pass-through, so the export stays byte-for-byte
* identical to the un-formatted behaviour.
*/
export interface ExportFieldMeta {
name: string;
type?: string;
label?: string;
options?: Array<{ label?: string; value?: unknown }>;
/** Target object for lookup / master_detail / user fields. */
reference?: string;
/** Field on the referenced record to show as its label. */
displayField?: string;
}
/** Field types whose stored value points at another record. */
const REFERENCE_TYPES = new Set(['lookup', 'master_detail', 'user', 'reference', 'tree']);
/** Field types whose stored value maps to a static option label. */
const OPTION_TYPES = new Set(['select', 'radio']);
const MULTI_OPTION_TYPES = new Set(['multiselect', 'checkboxes', 'tags']);
/**
* Keys tried, in order, to derive a referenced record's display value when the
* field carries no explicit `displayField`.
*/
const NAME_KEY_FALLBACKS = [
'name', 'title', 'label', 'full_name', 'fullName', 'display_name', 'username', 'email',
];
/**
* Build a field-name → metadata map from an object schema (best-effort).
*
* Accepts both shapes `fields` appears in across the stack: the runtime
* `ObjectSchema.fields` is a `Record<fieldName, FieldDefinition>` object map
* (the form served by the engine registry / `getMetaItem`), while some callers
* and fixtures hand back a plain `FieldDefinition[]` array. A field's name is
* taken from its own `name`, falling back to the map key.
*/
export function buildFieldMetaMap(schema: unknown): Map<string, ExportFieldMeta> {
const map = new Map<string, ExportFieldMeta>();
const fields = (schema as { fields?: unknown })?.fields;
// Normalize either shape to a list of [name, definition] entries.
let entries: Array<[string, any]>;
if (Array.isArray(fields)) {
entries = fields
.filter((f) => f && typeof f === 'object')
.map((f) => [typeof f.name === 'string' ? f.name : '', f] as [string, any]);
} else if (fields && typeof fields === 'object') {
entries = Object.entries(fields as Record<string, any>).map(
([key, def]) => [
def && typeof def === 'object' && typeof def.name === 'string' ? def.name : key,
def,
] as [string, any],
);
} else {
return map;
}
for (const [name, f] of entries) {
if (!name || !f || typeof f !== 'object') continue;
map.set(name, {
name,
type: typeof f.type === 'string' ? f.type : undefined,
label: typeof f.label === 'string' ? f.label : undefined,
options: Array.isArray(f.options) ? f.options : undefined,
reference: typeof f.reference === 'string' ? f.reference : undefined,
displayField: typeof f.displayField === 'string' ? f.displayField : undefined,
});
}
return map;
}
/**
* Reference-typed field names that should be `$expand`-ed so their stored ids
* resolve to the referenced record (and thus to a readable name).
*/
export function referenceFieldNames(metaMap: Map<string, ExportFieldMeta>): string[] {
const out: string[] = [];
for (const meta of metaMap.values()) {
if (meta.type && REFERENCE_TYPES.has(meta.type) && meta.reference) out.push(meta.name);
}
return out;
}
/** Header label for a column: schema label when present, else the field name. */
export function headerLabel(field: string, metaMap: Map<string, ExportFieldMeta>): string {
return metaMap.get(field)?.label || field;
}
function pad2(n: number): string {
return n < 10 ? `0${n}` : String(n);
}
function toDate(value: unknown): Date | null {
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
if (typeof value === 'number' || typeof value === 'string') {
const d = new Date(value);
return Number.isNaN(d.getTime()) ? null : d;
}
return null;
}
/** `YYYY-MM-DD` (date) or `YYYY-MM-DD HH:mm:ss` (datetime), in UTC. */
function formatDate(value: unknown, withTime: boolean): unknown {
const d = toDate(value);
if (!d) return value;
const ymd = `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
if (!withTime) return ymd;
return `${ymd} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}`;
}
function optionLabel(value: unknown, options?: Array<{ label?: string; value?: unknown }>): unknown {
if (!options) return value;
const hit = options.find((o) => o && o.value === value);
return hit?.label ?? value;
}
function displayFromRecord(rec: Record<string, unknown>, displayField?: string): string {
if (displayField && rec[displayField] != null) return String(rec[displayField]);
for (const k of NAME_KEY_FALLBACKS) {
const v = rec[k];
if (v != null && typeof v !== 'object') return String(v);
}
if (rec.id != null) return String(rec.id);
try { return JSON.stringify(rec); } catch { return String(rec); }
}
function formatReference(value: unknown, displayField?: string): unknown {
const one = (v: unknown): unknown =>
v && typeof v === 'object' ? displayFromRecord(v as Record<string, unknown>, displayField) : v;
if (Array.isArray(value)) return value.map(one).join(', ');
return one(value);
}
/** Format one storage value into a display value using its field metadata. */
export function formatCellValue(value: unknown, meta?: ExportFieldMeta): unknown {
if (value === null || value === undefined) return value;
if (!meta || !meta.type) return value;
const t = meta.type;
if (t === 'boolean' || t === 'toggle') {
if (value === true || value === 'true' || value === 1) return '是';
if (value === false || value === 'false' || value === 0) return '否';
return value;
}
if (OPTION_TYPES.has(t)) return optionLabel(value, meta.options);
if (MULTI_OPTION_TYPES.has(t)) {
const arr = Array.isArray(value) ? value : [value];
return arr.map((v) => optionLabel(v, meta.options)).join(', ');
}
if (t === 'date') return formatDate(value, false);
if (t === 'datetime') return formatDate(value, true);
if (REFERENCE_TYPES.has(t)) return formatReference(value, meta.displayField);
return value;
}
/** Ordered display cells for one row — the CSV / XLSX column path. */
export function formatRowCells(
row: Record<string, unknown>,
fields: string[],
metaMap: Map<string, ExportFieldMeta>,
): unknown[] {
return fields.map((f) => formatCellValue(row?.[f], metaMap.get(f)));
}
/**
* Format a row for JSON output: readable values for known fields, every other
* key left untouched. Returns the original object reference when nothing needs
* formatting so the stream stays byte-identical to the un-formatted path.
*/
export function formatRowForJson(
row: Record<string, unknown>,
metaMap: Map<string, ExportFieldMeta>,
): Record<string, unknown> {
if (metaMap.size === 0 || !row || typeof row !== 'object') return row;
let copy: Record<string, unknown> | null = null;
for (const key of Object.keys(row)) {
const meta = metaMap.get(key);
if (!meta) continue;
const formatted = formatCellValue(row[key], meta);
if (formatted !== row[key]) {
if (!copy) copy = { ...row };
copy[key] = formatted;
}
}
return copy ?? row;
}