juxscript 1.1.237 → 1.1.239
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +3 -3
- package/lib/data/DataPipeline.d.ts +60 -112
- package/lib/data/DataPipeline.d.ts.map +1 -1
- package/lib/data/DataPipeline.js +170 -141
- package/lib/data/DataPipeline.ts +211 -154
- package/package.json +1 -1
package/lib/data/DataPipeline.ts
CHANGED
|
@@ -13,97 +13,148 @@ export interface FetchOptions {
|
|
|
13
13
|
mimeType?: 'csv' | 'tsv' | 'json' | 'auto';
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export interface GatherOptions extends FetchOptions {
|
|
17
|
+
/** Authorization bearer token */
|
|
18
|
+
bearer?: string;
|
|
19
|
+
/** API key (sent as x-api-key header) */
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
/** Custom headers for URL fetches */
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
/** IndexedDB database name override */
|
|
24
|
+
dbName?: string;
|
|
25
|
+
/** IndexedDB store name override */
|
|
26
|
+
storeName?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IntoOptions {
|
|
30
|
+
/** Override which columns to use (for Table targets) */
|
|
31
|
+
columns?: string[];
|
|
32
|
+
/** Column name → display label mapping */
|
|
33
|
+
columnLabels?: Record<string, string>;
|
|
34
|
+
/** Key for option labels (Dropdown/Select/List targets) */
|
|
35
|
+
labelKey?: string;
|
|
36
|
+
/** Key for option values (Dropdown/Select targets) */
|
|
37
|
+
valueKey?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
16
40
|
/**
|
|
17
41
|
* DataPipeline — Headless, promise-based data loading and transformation.
|
|
18
42
|
*
|
|
19
43
|
* Usage:
|
|
20
|
-
* const df = await
|
|
21
|
-
* const df = await
|
|
22
|
-
* const df = await
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* await data.fromUrl(url)
|
|
26
|
-
* .transform(df => df.select('name', 'age').filter(r => r.age > 30))
|
|
27
|
-
* .into(myTable);
|
|
28
|
-
*
|
|
29
|
-
* await data.fromStorage('encounters')
|
|
30
|
-
* .transform(df => df.distinct('provider'))
|
|
31
|
-
* .into(myDropdown, { labelKey: 'provider', valueKey: 'provider' });
|
|
44
|
+
* const df = await jux.gather('raw/myfile.xlsx').result();
|
|
45
|
+
* const df = await jux.gather('https://api.com/data', { bearer: 'xxx' }).result();
|
|
46
|
+
* const df = await jux.gather(myUpload).select('name').result();
|
|
47
|
+
* const df = await jux.gather([{a:1}, {b:2}]).result();
|
|
48
|
+
* await jux.gather(key).filter(r => r.active).intoTable(myTable);
|
|
32
49
|
*/
|
|
33
50
|
export class DataPipeline {
|
|
34
51
|
private _driver: TabularDriver;
|
|
35
52
|
private _pending: Promise<DataFrame> | null = null;
|
|
36
53
|
private _transforms: ((df: DataFrame) => DataFrame)[] = [];
|
|
54
|
+
private _gatherOptions: GatherOptions;
|
|
37
55
|
|
|
38
56
|
constructor(options: PipelineOptions = {}) {
|
|
39
57
|
this._driver = new TabularDriver(
|
|
40
58
|
options.dbName ?? 'jux-dataframes',
|
|
41
59
|
options.storeName ?? 'frames'
|
|
42
60
|
);
|
|
61
|
+
this._gatherOptions = {};
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
/* ═══════════════════════════════════════════════════
|
|
46
|
-
*
|
|
65
|
+
* SMART SOURCE DETECTION
|
|
47
66
|
* ═══════════════════════════════════════════════════ */
|
|
48
67
|
|
|
49
68
|
/**
|
|
50
|
-
*
|
|
69
|
+
* Auto-detect source type and load accordingly.
|
|
70
|
+
*
|
|
71
|
+
* - string starting with http/https → URL fetch
|
|
72
|
+
* - string → IndexedDB storage key
|
|
73
|
+
* - File → parse file
|
|
74
|
+
* - Array → row objects
|
|
75
|
+
* - DataFrame → clone
|
|
76
|
+
* - object with .storageKey → FileUpload cached key
|
|
77
|
+
* - object with string keys + array values → column-oriented data
|
|
51
78
|
*/
|
|
52
|
-
|
|
79
|
+
from(source: any, options: GatherOptions = {}): this {
|
|
53
80
|
this._transforms = [];
|
|
54
|
-
this.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
this._gatherOptions = options;
|
|
82
|
+
|
|
83
|
+
// Update driver if custom db/store
|
|
84
|
+
if (options.dbName || options.storeName) {
|
|
85
|
+
this._driver = new TabularDriver(
|
|
86
|
+
options.dbName ?? 'jux-dataframes',
|
|
87
|
+
options.storeName ?? 'frames'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
58
90
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
91
|
+
if (source instanceof DataFrame) {
|
|
92
|
+
this._pending = Promise.resolve(source.clone());
|
|
93
|
+
} else if (Array.isArray(source)) {
|
|
94
|
+
this._pending = Promise.resolve(new DataFrame(source));
|
|
95
|
+
} else if (typeof source === 'string') {
|
|
96
|
+
if (/^https?:\/\//i.test(source)) {
|
|
97
|
+
this._pending = this._loadFromUrl(source, options);
|
|
98
|
+
} else {
|
|
99
|
+
this._pending = this._loadFromStorage(source);
|
|
100
|
+
}
|
|
101
|
+
} else if (source instanceof File) {
|
|
102
|
+
this._pending = this._loadFromFile(source, options);
|
|
103
|
+
} else if (source && typeof source === 'object' && 'storageKey' in source) {
|
|
104
|
+
// FileUpload component with .cache() enabled
|
|
105
|
+
const key = source.storageKey;
|
|
106
|
+
if (!key) {
|
|
107
|
+
this._pending = Promise.reject(
|
|
108
|
+
new Error('FileUpload has no cached storage key. Ensure .cache() is enabled and a file has been uploaded.')
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
this._pending = this._loadFromStorage(key);
|
|
112
|
+
}
|
|
113
|
+
} else if (source && typeof source === 'object' && !Array.isArray(source)) {
|
|
114
|
+
// Column-oriented: { col1: [...], col2: [...] }
|
|
115
|
+
this._pending = Promise.resolve(new DataFrame(source));
|
|
116
|
+
} else {
|
|
117
|
+
this._pending = Promise.reject(new Error('Unrecognized source type passed to gather()'));
|
|
118
|
+
}
|
|
62
119
|
|
|
63
|
-
throw new Error(`No data found in storage with key: "${key}"`);
|
|
64
|
-
})();
|
|
65
120
|
return this;
|
|
66
121
|
}
|
|
67
122
|
|
|
68
|
-
|
|
69
|
-
*
|
|
70
|
-
*/
|
|
71
|
-
|
|
123
|
+
/* ═══════════════════════════════════════════════════
|
|
124
|
+
* EXPLICIT SOURCES (still available)
|
|
125
|
+
* ═══════════════════════════════════════════════════ */
|
|
126
|
+
|
|
127
|
+
fromStorage(key: string): this {
|
|
128
|
+
this._transforms = [];
|
|
129
|
+
this._pending = this._loadFromStorage(key);
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fromUrl(url: string, options: GatherOptions = {}): this {
|
|
72
134
|
this._transforms = [];
|
|
135
|
+
this._gatherOptions = options;
|
|
73
136
|
this._pending = this._loadFromUrl(url, options);
|
|
74
137
|
return this;
|
|
75
138
|
}
|
|
76
139
|
|
|
77
|
-
/**
|
|
78
|
-
* Load from a File object directly
|
|
79
|
-
*/
|
|
80
140
|
fromFile(file: File, options: FetchOptions = {}): this {
|
|
81
141
|
this._transforms = [];
|
|
82
142
|
this._pending = this._loadFromFile(file, options);
|
|
83
143
|
return this;
|
|
84
144
|
}
|
|
85
145
|
|
|
86
|
-
/**
|
|
87
|
-
* Load from an array of row objects
|
|
88
|
-
*/
|
|
89
146
|
fromRows(rows: Record<string, any>[], columns?: string[]): this {
|
|
90
147
|
this._transforms = [];
|
|
91
148
|
this._pending = Promise.resolve(new DataFrame(rows, { columns }));
|
|
92
149
|
return this;
|
|
93
150
|
}
|
|
94
151
|
|
|
95
|
-
/**
|
|
96
|
-
* Load from column-oriented data
|
|
97
|
-
*/
|
|
98
152
|
fromColumns(columns: Record<string, any[]>): this {
|
|
99
153
|
this._transforms = [];
|
|
100
154
|
this._pending = Promise.resolve(new DataFrame(columns));
|
|
101
155
|
return this;
|
|
102
156
|
}
|
|
103
157
|
|
|
104
|
-
/**
|
|
105
|
-
* Load from a JSON response (array of objects or column-oriented)
|
|
106
|
-
*/
|
|
107
158
|
fromJson(json: Record<string, any>[] | Record<string, any[]> | string): this {
|
|
108
159
|
this._transforms = [];
|
|
109
160
|
const parsed = typeof json === 'string' ? JSON.parse(json) : json;
|
|
@@ -111,13 +162,6 @@ export class DataPipeline {
|
|
|
111
162
|
return this;
|
|
112
163
|
}
|
|
113
164
|
|
|
114
|
-
/**
|
|
115
|
-
* Load from a FileUpload component's cached storage key.
|
|
116
|
-
* Requires .cache() enabled on the FileUpload and a file already uploaded.
|
|
117
|
-
*
|
|
118
|
-
* Usage:
|
|
119
|
-
* await data().fromUpload(myUpload).select('name').into(myTable);
|
|
120
|
-
*/
|
|
121
165
|
fromUpload(upload: any): this {
|
|
122
166
|
this._transforms = [];
|
|
123
167
|
const key = upload.storageKey;
|
|
@@ -126,94 +170,52 @@ export class DataPipeline {
|
|
|
126
170
|
new Error('FileUpload has no cached storage key. Ensure .cache() is enabled and a file has been uploaded.')
|
|
127
171
|
);
|
|
128
172
|
} else {
|
|
129
|
-
this._pending = this.
|
|
130
|
-
if (!df) throw new Error(`No cached data found for key: "${key}"`);
|
|
131
|
-
return df;
|
|
132
|
-
});
|
|
173
|
+
this._pending = this._loadFromStorage(key);
|
|
133
174
|
}
|
|
134
175
|
return this;
|
|
135
176
|
}
|
|
136
177
|
|
|
137
|
-
/**
|
|
138
|
-
* Start from an existing DataFrame
|
|
139
|
-
*/
|
|
140
|
-
from(df: DataFrame): this {
|
|
141
|
-
this._transforms = [];
|
|
142
|
-
this._pending = Promise.resolve(df.clone());
|
|
143
|
-
return this;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
178
|
/* ═══════════════════════════════════════════════════
|
|
147
|
-
* TRANSFORMS (chained, lazy — applied on
|
|
179
|
+
* TRANSFORMS (chained, lazy — applied on terminal)
|
|
148
180
|
* ═══════════════════════════════════════════════════ */
|
|
149
181
|
|
|
150
|
-
/**
|
|
151
|
-
* Apply an arbitrary transform
|
|
152
|
-
*/
|
|
153
182
|
transform(fn: (df: DataFrame) => DataFrame): this {
|
|
154
183
|
this._transforms.push(fn);
|
|
155
184
|
return this;
|
|
156
185
|
}
|
|
157
186
|
|
|
158
|
-
/**
|
|
159
|
-
* Select columns
|
|
160
|
-
*/
|
|
161
187
|
select(...cols: string[]): this {
|
|
162
188
|
return this.transform(df => df.select(...cols));
|
|
163
189
|
}
|
|
164
190
|
|
|
165
|
-
/**
|
|
166
|
-
* Filter rows
|
|
167
|
-
*/
|
|
168
191
|
filter(predicate: (row: Record<string, any>, index: number) => boolean): this {
|
|
169
192
|
return this.transform(df => df.filter(predicate));
|
|
170
193
|
}
|
|
171
194
|
|
|
172
|
-
/**
|
|
173
|
-
* Sort by column
|
|
174
|
-
*/
|
|
175
195
|
sort(column: string, descending?: boolean): this {
|
|
176
196
|
return this.transform(df => df.sort(column, descending));
|
|
177
197
|
}
|
|
178
198
|
|
|
179
|
-
/**
|
|
180
|
-
* Take first N rows
|
|
181
|
-
*/
|
|
182
199
|
head(n: number = 5): this {
|
|
183
200
|
return this.transform(df => df.head(n));
|
|
184
201
|
}
|
|
185
202
|
|
|
186
|
-
/**
|
|
187
|
-
* Take last N rows
|
|
188
|
-
*/
|
|
189
203
|
tail(n: number = 5): this {
|
|
190
204
|
return this.transform(df => df.tail(n));
|
|
191
205
|
}
|
|
192
206
|
|
|
193
|
-
/**
|
|
194
|
-
* Rename columns
|
|
195
|
-
*/
|
|
196
207
|
rename(mapping: Record<string, string>): this {
|
|
197
208
|
return this.transform(df => df.rename(mapping));
|
|
198
209
|
}
|
|
199
210
|
|
|
200
|
-
/**
|
|
201
|
-
* Add a computed column
|
|
202
|
-
*/
|
|
203
211
|
withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this {
|
|
204
212
|
return this.transform(df => df.withColumn(name, fn));
|
|
205
213
|
}
|
|
206
214
|
|
|
207
|
-
/**
|
|
208
|
-
* Drop nulls
|
|
209
|
-
*/
|
|
210
215
|
dropna(columns?: string[]): this {
|
|
211
216
|
return this.transform(df => df.dropna(columns));
|
|
212
217
|
}
|
|
213
218
|
|
|
214
|
-
/**
|
|
215
|
-
* Drop duplicates
|
|
216
|
-
*/
|
|
217
219
|
dropDuplicates(columns?: string[]): this {
|
|
218
220
|
return this.transform(df => df.dropDuplicates(columns));
|
|
219
221
|
}
|
|
@@ -222,12 +224,8 @@ export class DataPipeline {
|
|
|
222
224
|
* TERMINAL OPERATIONS
|
|
223
225
|
* ═══════════════════════════════════════════════════ */
|
|
224
226
|
|
|
225
|
-
/**
|
|
226
|
-
* Execute the pipeline and return the DataFrame
|
|
227
|
-
*/
|
|
228
227
|
async result(): Promise<DataFrame> {
|
|
229
|
-
if (!this._pending) throw new Error('No data source specified. Call
|
|
230
|
-
|
|
228
|
+
if (!this._pending) throw new Error('No data source specified. Call gather(source) or from*(source) first.');
|
|
231
229
|
let df = await this._pending;
|
|
232
230
|
for (const fn of this._transforms) {
|
|
233
231
|
df = fn(df);
|
|
@@ -235,82 +233,107 @@ export class DataPipeline {
|
|
|
235
233
|
return df;
|
|
236
234
|
}
|
|
237
235
|
|
|
238
|
-
/**
|
|
239
|
-
* Execute and return rows as plain objects
|
|
240
|
-
*/
|
|
241
236
|
async toRows(): Promise<Record<string, any>[]> {
|
|
242
237
|
return (await this.result()).toRows();
|
|
243
238
|
}
|
|
244
239
|
|
|
245
|
-
/**
|
|
246
|
-
* Execute and return a single column as an array
|
|
247
|
-
*/
|
|
248
240
|
async toArray(column: string): Promise<any[]> {
|
|
249
241
|
return (await this.result()).col(column);
|
|
250
242
|
}
|
|
251
243
|
|
|
252
|
-
/**
|
|
253
|
-
* Execute and return distinct values from a column
|
|
254
|
-
*/
|
|
255
244
|
async toDistinct(column: string): Promise<any[]> {
|
|
256
245
|
return (await this.result()).distinct(column);
|
|
257
246
|
}
|
|
258
247
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
248
|
+
async toOptions(labelKey: string, valueKey?: string): Promise<{ label: string; value: any }[]> {
|
|
249
|
+
const df = await this.result();
|
|
250
|
+
const vKey = valueKey ?? labelKey;
|
|
251
|
+
return df.toRows().map(row => ({
|
|
252
|
+
label: String(row[labelKey] ?? ''),
|
|
253
|
+
value: row[vKey]
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async toList(column: string): Promise<string[]> {
|
|
258
|
+
const df = await this.result();
|
|
259
|
+
return df.col(column).map(v => String(v ?? ''));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async intoTable(table: any, columnLabels?: Record<string, string>): Promise<DataFrame> {
|
|
263
|
+
const df = await this.result();
|
|
264
|
+
const columnDefs = df.columns.map(col => ({
|
|
265
|
+
key: col,
|
|
266
|
+
label: columnLabels?.[col] ?? col
|
|
267
|
+
}));
|
|
268
|
+
table.columns(columnDefs).rows(df.toRows());
|
|
269
|
+
return df;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async intoDropdown(dropdown: any, labelKey: string, valueKey?: string): Promise<DataFrame> {
|
|
273
|
+
const df = await this.result();
|
|
274
|
+
const vKey = valueKey ?? labelKey;
|
|
275
|
+
const opts = df.toRows().map(row => ({
|
|
276
|
+
label: String(row[labelKey] ?? ''),
|
|
277
|
+
value: row[vKey]
|
|
278
|
+
}));
|
|
279
|
+
dropdown.options(opts);
|
|
280
|
+
return df;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async intoList(list: any, column: string): Promise<DataFrame> {
|
|
284
|
+
const df = await this.result();
|
|
285
|
+
list.items(df.col(column).map((v: any) => String(v ?? '')));
|
|
286
|
+
return df;
|
|
287
|
+
}
|
|
288
|
+
|
|
269
289
|
async into(target: any, options: IntoOptions = {}): Promise<DataFrame> {
|
|
270
290
|
const df = await this.result();
|
|
271
291
|
const rows = df.toRows();
|
|
272
292
|
|
|
273
|
-
// Table-like: has .columns() and .rows()
|
|
274
293
|
if (typeof target.columns === 'function' && typeof target.rows === 'function') {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
294
|
+
const cols = options.columns ?? df.columns;
|
|
295
|
+
const columnDefs = cols.map((col: string) => ({
|
|
296
|
+
key: col,
|
|
297
|
+
label: options.columnLabels?.[col] ?? col
|
|
298
|
+
}));
|
|
279
299
|
target.columns(columnDefs).rows(rows);
|
|
280
300
|
return df;
|
|
281
301
|
}
|
|
282
302
|
|
|
283
|
-
// DataFrameComponent: has .fromData()
|
|
284
303
|
if (typeof target.fromData === 'function') {
|
|
285
304
|
target.fromData(rows);
|
|
286
305
|
return df;
|
|
287
306
|
}
|
|
288
307
|
|
|
289
|
-
// Dropdown/Select-like: has .options()
|
|
290
308
|
if (typeof target.options === 'function') {
|
|
291
|
-
|
|
292
|
-
|
|
309
|
+
if (!options.labelKey) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
'Cannot push into dropdown/select without labelKey. ' +
|
|
312
|
+
'Use .intoDropdown(target, labelKey, valueKey) or pass { labelKey, valueKey } to .into()'
|
|
313
|
+
);
|
|
314
|
+
}
|
|
293
315
|
const opts = rows.map(row => ({
|
|
294
|
-
label: String(row[labelKey] ?? ''),
|
|
295
|
-
value: row[valueKey]
|
|
316
|
+
label: String(row[options.labelKey!] ?? ''),
|
|
317
|
+
value: row[options.valueKey ?? options.labelKey!]
|
|
296
318
|
}));
|
|
297
319
|
target.options(opts);
|
|
298
320
|
return df;
|
|
299
321
|
}
|
|
300
322
|
|
|
301
|
-
// List-like: has .items()
|
|
302
323
|
if (typeof target.items === 'function') {
|
|
303
|
-
|
|
304
|
-
|
|
324
|
+
if (!options.labelKey) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
'Cannot push into list without labelKey. ' +
|
|
327
|
+
'Use .intoList(target, column) or pass { labelKey } to .into()'
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
target.items(rows.map(row => String(row[options.labelKey!] ?? '')));
|
|
305
331
|
return df;
|
|
306
332
|
}
|
|
307
333
|
|
|
308
|
-
throw new Error(
|
|
334
|
+
throw new Error('Target does not have a recognized API (.columns/.rows, .fromData, .options, or .items)');
|
|
309
335
|
}
|
|
310
336
|
|
|
311
|
-
/**
|
|
312
|
-
* Execute and persist to IndexedDB
|
|
313
|
-
*/
|
|
314
337
|
async store(key: string, metadata?: Record<string, any>): Promise<DataFrame> {
|
|
315
338
|
const df = await this.result();
|
|
316
339
|
await this._driver.store(key, df, metadata);
|
|
@@ -321,17 +344,49 @@ export class DataPipeline {
|
|
|
321
344
|
* PRIVATE
|
|
322
345
|
* ═══════════════════════════════════════════════════ */
|
|
323
346
|
|
|
324
|
-
private async
|
|
347
|
+
private async _loadFromStorage(key: string): Promise<DataFrame> {
|
|
348
|
+
// Try by name first, then by id
|
|
349
|
+
let df = await this._driver.loadByName(key);
|
|
350
|
+
if (df) return df;
|
|
351
|
+
|
|
352
|
+
df = await this._driver.load(key);
|
|
353
|
+
if (df) return df;
|
|
354
|
+
|
|
355
|
+
throw new Error(`No data found in storage with key: "${key}"`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private async _loadFromUrl(url: string, options: GatherOptions): Promise<DataFrame> {
|
|
325
359
|
const mimeType = options.mimeType ?? this._detectMimeType(url);
|
|
326
360
|
|
|
361
|
+
// Build headers from auth options
|
|
362
|
+
const headers: Record<string, string> = { ...options.headers };
|
|
363
|
+
if (options.bearer) {
|
|
364
|
+
headers['Authorization'] = `Bearer ${options.bearer}`;
|
|
365
|
+
}
|
|
366
|
+
if (options.apiKey) {
|
|
367
|
+
headers['x-api-key'] = options.apiKey;
|
|
368
|
+
}
|
|
369
|
+
|
|
327
370
|
if (mimeType === 'json') {
|
|
328
|
-
const response = await fetch(url);
|
|
371
|
+
const response = await fetch(url, { headers });
|
|
329
372
|
if (!response.ok) throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);
|
|
330
373
|
const json = await response.json();
|
|
331
374
|
return new DataFrame(json);
|
|
332
375
|
}
|
|
333
376
|
|
|
334
|
-
// CSV/TSV —
|
|
377
|
+
// CSV/TSV — fetch with headers, then parse
|
|
378
|
+
if (Object.keys(headers).length > 0) {
|
|
379
|
+
const response = await fetch(url, { headers });
|
|
380
|
+
if (!response.ok) throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);
|
|
381
|
+
const text = await response.text();
|
|
382
|
+
return this._driver.parseCSV(text, {
|
|
383
|
+
autoDetectDelimiter: !options.delimiter,
|
|
384
|
+
delimiter: options.delimiter,
|
|
385
|
+
hasHeader: options.hasHeader ?? true,
|
|
386
|
+
headerRow: options.headerRow
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
335
390
|
return this._driver.fetch(url, {
|
|
336
391
|
autoDetectDelimiter: !options.delimiter,
|
|
337
392
|
delimiter: options.delimiter,
|
|
@@ -370,26 +425,28 @@ export class DataPipeline {
|
|
|
370
425
|
}
|
|
371
426
|
}
|
|
372
427
|
|
|
373
|
-
export interface IntoOptions {
|
|
374
|
-
/** Override which columns to use (for Table targets) */
|
|
375
|
-
columns?: string[];
|
|
376
|
-
/** Column name → display label mapping */
|
|
377
|
-
columnLabels?: Record<string, string>;
|
|
378
|
-
/** Key for option labels (Dropdown/Select/List targets) */
|
|
379
|
-
labelKey?: string;
|
|
380
|
-
/** Key for option values (Dropdown/Select targets) */
|
|
381
|
-
valueKey?: string;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
428
|
/**
|
|
385
|
-
* Factory function —
|
|
429
|
+
* Factory function — primary entry point via jux.gather()
|
|
386
430
|
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
431
|
+
* Smart single-arg:
|
|
432
|
+
* jux.gather('raw/myfile.xlsx') // storage key
|
|
433
|
+
* jux.gather('https://api.com/data', { bearer: 'xxx' }) // URL with auth
|
|
434
|
+
* jux.gather(myUpload) // FileUpload
|
|
435
|
+
* jux.gather([{a:1}, {b:2}]) // inline rows
|
|
436
|
+
* jux.gather(existingDf) // clone DataFrame
|
|
389
437
|
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
438
|
+
* Explicit source:
|
|
439
|
+
* jux.gather().fromStorage(key)
|
|
440
|
+
* jux.gather().fromUrl(url, opts)
|
|
392
441
|
*/
|
|
393
|
-
export function
|
|
394
|
-
|
|
442
|
+
export function gather(source?: any, options?: GatherOptions): DataPipeline {
|
|
443
|
+
const pipe = new DataPipeline(
|
|
444
|
+
options?.dbName || options?.storeName
|
|
445
|
+
? { dbName: options.dbName, storeName: options.storeName }
|
|
446
|
+
: {}
|
|
447
|
+
);
|
|
448
|
+
if (source !== undefined) {
|
|
449
|
+
pipe.from(source, options ?? {});
|
|
450
|
+
}
|
|
451
|
+
return pipe;
|
|
395
452
|
}
|