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.
@@ -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 data.fromUrl('https://example.com/report.csv').result();
21
- * const df = await data.fromStorage('patients').result();
22
- * const df = await data.fromJson(apiResponse).result();
23
- *
24
- * // Transform and distribute:
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
- * SOURCES
65
+ * SMART SOURCE DETECTION
47
66
  * ═══════════════════════════════════════════════════ */
48
67
 
49
68
  /**
50
- * Load from IndexedDB by key (searches by name first, then by id)
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
- fromStorage(key: string): this {
79
+ from(source: any, options: GatherOptions = {}): this {
53
80
  this._transforms = [];
54
- this._pending = (async () => {
55
- // Try by name index first
56
- let df = await this._driver.loadByName(key);
57
- if (df) return df;
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
- // Fall back to loading by primary key (id)
60
- df = await this._driver.load(key);
61
- if (df) return df;
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
- * Load from a URL (CSV, TSV, or JSON)
70
- */
71
- fromUrl(url: string, options: FetchOptions = {}): this {
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._driver.loadByName(key).then((df: any) => {
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 .result())
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 fromStorage(), fromUrl(), etc. first.');
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
- * Execute and push results into a jux component.
261
- *
262
- * Supported targets:
263
- * - Table: sets columns + rows
264
- * - Dropdown/Select: sets options from labelKey/valueKey
265
- * - List: sets items
266
- * - DataFrameComponent: loads data
267
- * - Any object with a .rows() or .options() or .items() method
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 columnDefs = (options.columns ?? df.columns).map((col: string) => {
276
- const label = options.columnLabels?.[col] ?? col;
277
- return { key: col, label };
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
- const labelKey = options.labelKey ?? df.columns[0];
292
- const valueKey = options.valueKey ?? (df.columns[1] ?? df.columns[0]);
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
- const key = options.labelKey ?? df.columns[0];
304
- target.items(rows.map(row => String(row[key] ?? '')));
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(`Target does not have a recognized API (.columns/.rows, .fromData, .options, or .items)`);
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 _loadFromUrl(url: string, options: FetchOptions): Promise<DataFrame> {
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 — delegate to driver
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 — the primary entry point
429
+ * Factory function — primary entry point via jux.gather()
386
430
  *
387
- * Usage:
388
- * import { data } from 'juxscript';
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
- * const df = await data().fromStorage('patients').select('name', 'dob').result();
391
- * await data().fromUrl(presignedUrl).filter(r => r.status === 'active').into(myTable);
438
+ * Explicit source:
439
+ * jux.gather().fromStorage(key)
440
+ * jux.gather().fromUrl(url, opts)
392
441
  */
393
- export function data(options: PipelineOptions = {}): DataPipeline {
394
- return new DataPipeline(options);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.237",
3
+ "version": "1.1.239",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",