juxscript 1.1.233 → 1.1.235
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 +2 -0
- package/index.d.ts.map +1 -1
- package/index.js +2 -0
- package/lib/components/dataframe.d.ts.map +1 -1
- package/lib/components/dataframe.ts +0 -1
- package/lib/components/fileupload.d.ts +40 -0
- package/lib/components/fileupload.d.ts.map +1 -1
- package/lib/components/fileupload.js +108 -10
- package/lib/components/fileupload.ts +123 -10
- package/lib/data/DataPipeline.d.ts +165 -0
- package/lib/data/DataPipeline.d.ts.map +1 -0
- package/lib/data/DataPipeline.js +324 -0
- package/lib/data/DataPipeline.ts +388 -0
- package/package.json +1 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { DataFrame } from '../storage/DataFrame.js';
|
|
2
|
+
export interface PipelineOptions {
|
|
3
|
+
dbName?: string;
|
|
4
|
+
storeName?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface FetchOptions {
|
|
7
|
+
delimiter?: string;
|
|
8
|
+
hasHeader?: boolean;
|
|
9
|
+
headerRow?: number;
|
|
10
|
+
mimeType?: 'csv' | 'tsv' | 'json' | 'auto';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* DataPipeline — Headless, promise-based data loading and transformation.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* const df = await data.fromUrl('https://example.com/report.csv').result();
|
|
17
|
+
* const df = await data.fromStorage('patients').result();
|
|
18
|
+
* const df = await data.fromJson(apiResponse).result();
|
|
19
|
+
*
|
|
20
|
+
* // Transform and distribute:
|
|
21
|
+
* await data.fromUrl(url)
|
|
22
|
+
* .transform(df => df.select('name', 'age').filter(r => r.age > 30))
|
|
23
|
+
* .into(myTable);
|
|
24
|
+
*
|
|
25
|
+
* await data.fromStorage('encounters')
|
|
26
|
+
* .transform(df => df.distinct('provider'))
|
|
27
|
+
* .into(myDropdown, { labelKey: 'provider', valueKey: 'provider' });
|
|
28
|
+
*/
|
|
29
|
+
export declare class DataPipeline {
|
|
30
|
+
private _driver;
|
|
31
|
+
private _pending;
|
|
32
|
+
private _transforms;
|
|
33
|
+
constructor(options?: PipelineOptions);
|
|
34
|
+
/**
|
|
35
|
+
* Load from IndexedDB by key
|
|
36
|
+
*/
|
|
37
|
+
fromStorage(key: string): this;
|
|
38
|
+
/**
|
|
39
|
+
* Load from a URL (CSV, TSV, or JSON)
|
|
40
|
+
*/
|
|
41
|
+
fromUrl(url: string, options?: FetchOptions): this;
|
|
42
|
+
/**
|
|
43
|
+
* Load from a File object directly
|
|
44
|
+
*/
|
|
45
|
+
fromFile(file: File, options?: FetchOptions): this;
|
|
46
|
+
/**
|
|
47
|
+
* Load from an array of row objects
|
|
48
|
+
*/
|
|
49
|
+
fromRows(rows: Record<string, any>[], columns?: string[]): this;
|
|
50
|
+
/**
|
|
51
|
+
* Load from column-oriented data
|
|
52
|
+
*/
|
|
53
|
+
fromColumns(columns: Record<string, any[]>): this;
|
|
54
|
+
/**
|
|
55
|
+
* Load from a JSON response (array of objects or column-oriented)
|
|
56
|
+
*/
|
|
57
|
+
fromJson(json: Record<string, any>[] | Record<string, any[]> | string): this;
|
|
58
|
+
/**
|
|
59
|
+
* Load from a FileUpload component's cached storage key.
|
|
60
|
+
* Requires .cache() enabled on the FileUpload and a file already uploaded.
|
|
61
|
+
*
|
|
62
|
+
* Usage:
|
|
63
|
+
* await data().fromUpload(myUpload).select('name').into(myTable);
|
|
64
|
+
*/
|
|
65
|
+
fromUpload(upload: any): this;
|
|
66
|
+
/**
|
|
67
|
+
* Start from an existing DataFrame
|
|
68
|
+
*/
|
|
69
|
+
from(df: DataFrame): this;
|
|
70
|
+
/**
|
|
71
|
+
* Apply an arbitrary transform
|
|
72
|
+
*/
|
|
73
|
+
transform(fn: (df: DataFrame) => DataFrame): this;
|
|
74
|
+
/**
|
|
75
|
+
* Select columns
|
|
76
|
+
*/
|
|
77
|
+
select(...cols: string[]): this;
|
|
78
|
+
/**
|
|
79
|
+
* Filter rows
|
|
80
|
+
*/
|
|
81
|
+
filter(predicate: (row: Record<string, any>, index: number) => boolean): this;
|
|
82
|
+
/**
|
|
83
|
+
* Sort by column
|
|
84
|
+
*/
|
|
85
|
+
sort(column: string, descending?: boolean): this;
|
|
86
|
+
/**
|
|
87
|
+
* Take first N rows
|
|
88
|
+
*/
|
|
89
|
+
head(n?: number): this;
|
|
90
|
+
/**
|
|
91
|
+
* Take last N rows
|
|
92
|
+
*/
|
|
93
|
+
tail(n?: number): this;
|
|
94
|
+
/**
|
|
95
|
+
* Rename columns
|
|
96
|
+
*/
|
|
97
|
+
rename(mapping: Record<string, string>): this;
|
|
98
|
+
/**
|
|
99
|
+
* Add a computed column
|
|
100
|
+
*/
|
|
101
|
+
withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this;
|
|
102
|
+
/**
|
|
103
|
+
* Drop nulls
|
|
104
|
+
*/
|
|
105
|
+
dropna(columns?: string[]): this;
|
|
106
|
+
/**
|
|
107
|
+
* Drop duplicates
|
|
108
|
+
*/
|
|
109
|
+
dropDuplicates(columns?: string[]): this;
|
|
110
|
+
/**
|
|
111
|
+
* Execute the pipeline and return the DataFrame
|
|
112
|
+
*/
|
|
113
|
+
result(): Promise<DataFrame>;
|
|
114
|
+
/**
|
|
115
|
+
* Execute and return rows as plain objects
|
|
116
|
+
*/
|
|
117
|
+
toRows(): Promise<Record<string, any>[]>;
|
|
118
|
+
/**
|
|
119
|
+
* Execute and return a single column as an array
|
|
120
|
+
*/
|
|
121
|
+
toArray(column: string): Promise<any[]>;
|
|
122
|
+
/**
|
|
123
|
+
* Execute and return distinct values from a column
|
|
124
|
+
*/
|
|
125
|
+
toDistinct(column: string): Promise<any[]>;
|
|
126
|
+
/**
|
|
127
|
+
* Execute and push results into a jux component.
|
|
128
|
+
*
|
|
129
|
+
* Supported targets:
|
|
130
|
+
* - Table: sets columns + rows
|
|
131
|
+
* - Dropdown/Select: sets options from labelKey/valueKey
|
|
132
|
+
* - List: sets items
|
|
133
|
+
* - DataFrameComponent: loads data
|
|
134
|
+
* - Any object with a .rows() or .options() or .items() method
|
|
135
|
+
*/
|
|
136
|
+
into(target: any, options?: IntoOptions): Promise<DataFrame>;
|
|
137
|
+
/**
|
|
138
|
+
* Execute and persist to IndexedDB
|
|
139
|
+
*/
|
|
140
|
+
store(key: string, metadata?: Record<string, any>): Promise<DataFrame>;
|
|
141
|
+
private _loadFromUrl;
|
|
142
|
+
private _loadFromFile;
|
|
143
|
+
private _detectMimeType;
|
|
144
|
+
}
|
|
145
|
+
export interface IntoOptions {
|
|
146
|
+
/** Override which columns to use (for Table targets) */
|
|
147
|
+
columns?: string[];
|
|
148
|
+
/** Column name → display label mapping */
|
|
149
|
+
columnLabels?: Record<string, string>;
|
|
150
|
+
/** Key for option labels (Dropdown/Select/List targets) */
|
|
151
|
+
labelKey?: string;
|
|
152
|
+
/** Key for option values (Dropdown/Select targets) */
|
|
153
|
+
valueKey?: string;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Factory function — the primary entry point
|
|
157
|
+
*
|
|
158
|
+
* Usage:
|
|
159
|
+
* import { data } from 'juxscript';
|
|
160
|
+
*
|
|
161
|
+
* const df = await data().fromStorage('patients').select('name', 'dob').result();
|
|
162
|
+
* await data().fromUrl(presignedUrl).filter(r => r.status === 'active').into(myTable);
|
|
163
|
+
*/
|
|
164
|
+
export declare function data(options?: PipelineOptions): DataPipeline;
|
|
165
|
+
//# sourceMappingURL=DataPipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DataPipeline.d.ts","sourceRoot":"","sources":["DataPipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { DataFrame } from '../storage/DataFrame.js';
|
|
2
|
+
import { TabularDriver } from '../storage/TabularDriver.js';
|
|
3
|
+
/**
|
|
4
|
+
* DataPipeline — Headless, promise-based data loading and transformation.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const df = await data.fromUrl('https://example.com/report.csv').result();
|
|
8
|
+
* const df = await data.fromStorage('patients').result();
|
|
9
|
+
* const df = await data.fromJson(apiResponse).result();
|
|
10
|
+
*
|
|
11
|
+
* // Transform and distribute:
|
|
12
|
+
* await data.fromUrl(url)
|
|
13
|
+
* .transform(df => df.select('name', 'age').filter(r => r.age > 30))
|
|
14
|
+
* .into(myTable);
|
|
15
|
+
*
|
|
16
|
+
* await data.fromStorage('encounters')
|
|
17
|
+
* .transform(df => df.distinct('provider'))
|
|
18
|
+
* .into(myDropdown, { labelKey: 'provider', valueKey: 'provider' });
|
|
19
|
+
*/
|
|
20
|
+
export class DataPipeline {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this._pending = null;
|
|
23
|
+
this._transforms = [];
|
|
24
|
+
this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
|
|
25
|
+
}
|
|
26
|
+
/* ═══════════════════════════════════════════════════
|
|
27
|
+
* SOURCES
|
|
28
|
+
* ═══════════════════════════════════════════════════ */
|
|
29
|
+
/**
|
|
30
|
+
* Load from IndexedDB by key
|
|
31
|
+
*/
|
|
32
|
+
fromStorage(key) {
|
|
33
|
+
this._transforms = [];
|
|
34
|
+
this._pending = this._driver.loadByName(key).then(df => {
|
|
35
|
+
if (!df)
|
|
36
|
+
throw new Error(`No data found in storage with key: "${key}"`);
|
|
37
|
+
return df;
|
|
38
|
+
});
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Load from a URL (CSV, TSV, or JSON)
|
|
43
|
+
*/
|
|
44
|
+
fromUrl(url, options = {}) {
|
|
45
|
+
this._transforms = [];
|
|
46
|
+
this._pending = this._loadFromUrl(url, options);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Load from a File object directly
|
|
51
|
+
*/
|
|
52
|
+
fromFile(file, options = {}) {
|
|
53
|
+
this._transforms = [];
|
|
54
|
+
this._pending = this._loadFromFile(file, options);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Load from an array of row objects
|
|
59
|
+
*/
|
|
60
|
+
fromRows(rows, columns) {
|
|
61
|
+
this._transforms = [];
|
|
62
|
+
this._pending = Promise.resolve(new DataFrame(rows, { columns }));
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Load from column-oriented data
|
|
67
|
+
*/
|
|
68
|
+
fromColumns(columns) {
|
|
69
|
+
this._transforms = [];
|
|
70
|
+
this._pending = Promise.resolve(new DataFrame(columns));
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Load from a JSON response (array of objects or column-oriented)
|
|
75
|
+
*/
|
|
76
|
+
fromJson(json) {
|
|
77
|
+
this._transforms = [];
|
|
78
|
+
const parsed = typeof json === 'string' ? JSON.parse(json) : json;
|
|
79
|
+
this._pending = Promise.resolve(new DataFrame(parsed));
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Load from a FileUpload component's cached storage key.
|
|
84
|
+
* Requires .cache() enabled on the FileUpload and a file already uploaded.
|
|
85
|
+
*
|
|
86
|
+
* Usage:
|
|
87
|
+
* await data().fromUpload(myUpload).select('name').into(myTable);
|
|
88
|
+
*/
|
|
89
|
+
fromUpload(upload) {
|
|
90
|
+
this._transforms = [];
|
|
91
|
+
const key = upload.storageKey;
|
|
92
|
+
if (!key) {
|
|
93
|
+
this._pending = Promise.reject(new Error('FileUpload has no cached storage key. Ensure .cache() is enabled and a file has been uploaded.'));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
this._pending = this._driver.loadByName(key).then((df) => {
|
|
97
|
+
if (!df)
|
|
98
|
+
throw new Error(`No cached data found for key: "${key}"`);
|
|
99
|
+
return df;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Start from an existing DataFrame
|
|
106
|
+
*/
|
|
107
|
+
from(df) {
|
|
108
|
+
this._transforms = [];
|
|
109
|
+
this._pending = Promise.resolve(df.clone());
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
/* ═══════════════════════════════════════════════════
|
|
113
|
+
* TRANSFORMS (chained, lazy — applied on .result())
|
|
114
|
+
* ═══════════════════════════════════════════════════ */
|
|
115
|
+
/**
|
|
116
|
+
* Apply an arbitrary transform
|
|
117
|
+
*/
|
|
118
|
+
transform(fn) {
|
|
119
|
+
this._transforms.push(fn);
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Select columns
|
|
124
|
+
*/
|
|
125
|
+
select(...cols) {
|
|
126
|
+
return this.transform(df => df.select(...cols));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Filter rows
|
|
130
|
+
*/
|
|
131
|
+
filter(predicate) {
|
|
132
|
+
return this.transform(df => df.filter(predicate));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Sort by column
|
|
136
|
+
*/
|
|
137
|
+
sort(column, descending) {
|
|
138
|
+
return this.transform(df => df.sort(column, descending));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Take first N rows
|
|
142
|
+
*/
|
|
143
|
+
head(n = 5) {
|
|
144
|
+
return this.transform(df => df.head(n));
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Take last N rows
|
|
148
|
+
*/
|
|
149
|
+
tail(n = 5) {
|
|
150
|
+
return this.transform(df => df.tail(n));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Rename columns
|
|
154
|
+
*/
|
|
155
|
+
rename(mapping) {
|
|
156
|
+
return this.transform(df => df.rename(mapping));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Add a computed column
|
|
160
|
+
*/
|
|
161
|
+
withColumn(name, fn) {
|
|
162
|
+
return this.transform(df => df.withColumn(name, fn));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Drop nulls
|
|
166
|
+
*/
|
|
167
|
+
dropna(columns) {
|
|
168
|
+
return this.transform(df => df.dropna(columns));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Drop duplicates
|
|
172
|
+
*/
|
|
173
|
+
dropDuplicates(columns) {
|
|
174
|
+
return this.transform(df => df.dropDuplicates(columns));
|
|
175
|
+
}
|
|
176
|
+
/* ═══════════════════════════════════════════════════
|
|
177
|
+
* TERMINAL OPERATIONS
|
|
178
|
+
* ═══════════════════════════════════════════════════ */
|
|
179
|
+
/**
|
|
180
|
+
* Execute the pipeline and return the DataFrame
|
|
181
|
+
*/
|
|
182
|
+
async result() {
|
|
183
|
+
if (!this._pending)
|
|
184
|
+
throw new Error('No data source specified. Call fromStorage(), fromUrl(), etc. first.');
|
|
185
|
+
let df = await this._pending;
|
|
186
|
+
for (const fn of this._transforms) {
|
|
187
|
+
df = fn(df);
|
|
188
|
+
}
|
|
189
|
+
return df;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Execute and return rows as plain objects
|
|
193
|
+
*/
|
|
194
|
+
async toRows() {
|
|
195
|
+
return (await this.result()).toRows();
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Execute and return a single column as an array
|
|
199
|
+
*/
|
|
200
|
+
async toArray(column) {
|
|
201
|
+
return (await this.result()).col(column);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Execute and return distinct values from a column
|
|
205
|
+
*/
|
|
206
|
+
async toDistinct(column) {
|
|
207
|
+
return (await this.result()).distinct(column);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Execute and push results into a jux component.
|
|
211
|
+
*
|
|
212
|
+
* Supported targets:
|
|
213
|
+
* - Table: sets columns + rows
|
|
214
|
+
* - Dropdown/Select: sets options from labelKey/valueKey
|
|
215
|
+
* - List: sets items
|
|
216
|
+
* - DataFrameComponent: loads data
|
|
217
|
+
* - Any object with a .rows() or .options() or .items() method
|
|
218
|
+
*/
|
|
219
|
+
async into(target, options = {}) {
|
|
220
|
+
const df = await this.result();
|
|
221
|
+
const rows = df.toRows();
|
|
222
|
+
// Table-like: has .columns() and .rows()
|
|
223
|
+
if (typeof target.columns === 'function' && typeof target.rows === 'function') {
|
|
224
|
+
const columnDefs = (options.columns ?? df.columns).map((col) => {
|
|
225
|
+
const label = options.columnLabels?.[col] ?? col;
|
|
226
|
+
return { key: col, label };
|
|
227
|
+
});
|
|
228
|
+
target.columns(columnDefs).rows(rows);
|
|
229
|
+
return df;
|
|
230
|
+
}
|
|
231
|
+
// DataFrameComponent: has .fromData()
|
|
232
|
+
if (typeof target.fromData === 'function') {
|
|
233
|
+
target.fromData(rows);
|
|
234
|
+
return df;
|
|
235
|
+
}
|
|
236
|
+
// Dropdown/Select-like: has .options()
|
|
237
|
+
if (typeof target.options === 'function') {
|
|
238
|
+
const labelKey = options.labelKey ?? df.columns[0];
|
|
239
|
+
const valueKey = options.valueKey ?? (df.columns[1] ?? df.columns[0]);
|
|
240
|
+
const opts = rows.map(row => ({
|
|
241
|
+
label: String(row[labelKey] ?? ''),
|
|
242
|
+
value: row[valueKey]
|
|
243
|
+
}));
|
|
244
|
+
target.options(opts);
|
|
245
|
+
return df;
|
|
246
|
+
}
|
|
247
|
+
// List-like: has .items()
|
|
248
|
+
if (typeof target.items === 'function') {
|
|
249
|
+
const key = options.labelKey ?? df.columns[0];
|
|
250
|
+
target.items(rows.map(row => String(row[key] ?? '')));
|
|
251
|
+
return df;
|
|
252
|
+
}
|
|
253
|
+
throw new Error(`Target does not have a recognized API (.columns/.rows, .fromData, .options, or .items)`);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Execute and persist to IndexedDB
|
|
257
|
+
*/
|
|
258
|
+
async store(key, metadata) {
|
|
259
|
+
const df = await this.result();
|
|
260
|
+
await this._driver.store(key, df, metadata);
|
|
261
|
+
return df;
|
|
262
|
+
}
|
|
263
|
+
/* ═══════════════════════════════════════════════════
|
|
264
|
+
* PRIVATE
|
|
265
|
+
* ═══════════════════════════════════════════════════ */
|
|
266
|
+
async _loadFromUrl(url, options) {
|
|
267
|
+
const mimeType = options.mimeType ?? this._detectMimeType(url);
|
|
268
|
+
if (mimeType === 'json') {
|
|
269
|
+
const response = await fetch(url);
|
|
270
|
+
if (!response.ok)
|
|
271
|
+
throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);
|
|
272
|
+
const json = await response.json();
|
|
273
|
+
return new DataFrame(json);
|
|
274
|
+
}
|
|
275
|
+
// CSV/TSV — delegate to driver
|
|
276
|
+
return this._driver.fetch(url, {
|
|
277
|
+
autoDetectDelimiter: !options.delimiter,
|
|
278
|
+
delimiter: options.delimiter,
|
|
279
|
+
hasHeader: options.hasHeader ?? true,
|
|
280
|
+
headerRow: options.headerRow
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async _loadFromFile(file, options) {
|
|
284
|
+
const isExcel = /\.(xlsx?|xls)$/i.test(file.name);
|
|
285
|
+
if (isExcel) {
|
|
286
|
+
const sheets = await this._driver.streamFileMultiSheet(file, {
|
|
287
|
+
headerRow: options.headerRow
|
|
288
|
+
});
|
|
289
|
+
const sheetNames = Object.keys(sheets);
|
|
290
|
+
if (sheetNames.length === 0)
|
|
291
|
+
throw new Error('No data found in file');
|
|
292
|
+
return sheets[sheetNames[0]];
|
|
293
|
+
}
|
|
294
|
+
const text = await file.text();
|
|
295
|
+
return this._driver.parseCSV(text, {
|
|
296
|
+
autoDetectDelimiter: !options.delimiter,
|
|
297
|
+
delimiter: options.delimiter,
|
|
298
|
+
hasHeader: options.hasHeader ?? true,
|
|
299
|
+
headerRow: options.headerRow
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
_detectMimeType(url) {
|
|
303
|
+
const lower = url.toLowerCase().split('?')[0];
|
|
304
|
+
if (lower.endsWith('.json'))
|
|
305
|
+
return 'json';
|
|
306
|
+
if (lower.endsWith('.tsv'))
|
|
307
|
+
return 'tsv';
|
|
308
|
+
if (lower.endsWith('.csv'))
|
|
309
|
+
return 'csv';
|
|
310
|
+
return 'auto';
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Factory function — the primary entry point
|
|
315
|
+
*
|
|
316
|
+
* Usage:
|
|
317
|
+
* import { data } from 'juxscript';
|
|
318
|
+
*
|
|
319
|
+
* const df = await data().fromStorage('patients').select('name', 'dob').result();
|
|
320
|
+
* await data().fromUrl(presignedUrl).filter(r => r.status === 'active').into(myTable);
|
|
321
|
+
*/
|
|
322
|
+
export function data(options = {}) {
|
|
323
|
+
return new DataPipeline(options);
|
|
324
|
+
}
|