@oxy-hq/sdk 0.1.5 → 0.2.0
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/README.md +288 -157
- package/dist/index.cjs +592 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +424 -40
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +424 -40
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +588 -96
- package/dist/index.mjs.map +1 -1
- package/dist/{postMessage-CufWf9ji.cjs → postMessage-B1J0jDRN.cjs} +15 -11
- package/dist/postMessage-B1J0jDRN.cjs.map +1 -0
- package/dist/{postMessage-CVS3MsL5.cjs → postMessage-BSNS3ccd.cjs} +1 -1
- package/dist/{postMessage-DLGITn0e.mjs → postMessage-BxdgtX8j.mjs} +15 -11
- package/dist/postMessage-BxdgtX8j.mjs.map +1 -0
- package/dist/{postMessage-DwfY0HM5.mjs → postMessage-D5wWgwcO.mjs} +1 -1
- package/package.json +12 -7
- package/dist/postMessage-CufWf9ji.cjs.map +0 -1
- package/dist/postMessage-DLGITn0e.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
// @oxy/sdk - TypeScript SDK for Oxy data platform
|
|
2
|
-
import { a as PostMessageAuthInvalidOriginError, c as PostMessageAuthTimeoutError, n as isInIframe, o as PostMessageAuthInvalidResponseError, r as requestAuthFromParent, s as PostMessageAuthNotInIframeError } from "./postMessage-
|
|
2
|
+
import { a as PostMessageAuthInvalidOriginError, c as PostMessageAuthTimeoutError, n as isInIframe, o as PostMessageAuthInvalidResponseError, r as requestAuthFromParent, s as PostMessageAuthNotInIframeError } from "./postMessage-BxdgtX8j.mjs";
|
|
3
3
|
import * as duckdb from "@duckdb/duckdb-wasm";
|
|
4
|
+
import React, { createContext, useContext, useEffect, useState } from "react";
|
|
4
5
|
|
|
5
6
|
//#region src/config.ts
|
|
6
7
|
/**
|
|
8
|
+
* Safely get environment variable in both Node.js and browser environments
|
|
9
|
+
*/
|
|
10
|
+
function getEnvVar(name) {
|
|
11
|
+
if (typeof process !== "undefined" && process.env) return process.env[name];
|
|
12
|
+
if (typeof import.meta !== "undefined" && import.meta.env) {
|
|
13
|
+
const viteValue = import.meta.env[`VITE_${name}`];
|
|
14
|
+
if (viteValue !== void 0) return viteValue;
|
|
15
|
+
return import.meta.env[name];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
7
19
|
* Creates an Oxy configuration from environment variables
|
|
8
20
|
*
|
|
9
21
|
* Environment variables:
|
|
@@ -17,16 +29,16 @@ import * as duckdb from "@duckdb/duckdb-wasm";
|
|
|
17
29
|
* @throws Error if required environment variables are missing
|
|
18
30
|
*/
|
|
19
31
|
function createConfig(overrides) {
|
|
20
|
-
const baseUrl = overrides?.baseUrl ||
|
|
21
|
-
const apiKey = overrides?.apiKey ||
|
|
22
|
-
const projectId = overrides?.projectId ||
|
|
32
|
+
const baseUrl = overrides?.baseUrl || getEnvVar("OXY_URL");
|
|
33
|
+
const apiKey = overrides?.apiKey || getEnvVar("OXY_API_KEY");
|
|
34
|
+
const projectId = overrides?.projectId || getEnvVar("OXY_PROJECT_ID");
|
|
23
35
|
if (!baseUrl) throw new Error("OXY_URL environment variable or baseUrl config is required");
|
|
24
36
|
if (!projectId) throw new Error("OXY_PROJECT_ID environment variable or projectId config is required");
|
|
25
37
|
return {
|
|
26
38
|
baseUrl: baseUrl.replace(/\/$/, ""),
|
|
27
39
|
apiKey,
|
|
28
40
|
projectId,
|
|
29
|
-
branch: overrides?.branch ||
|
|
41
|
+
branch: overrides?.branch || getEnvVar("OXY_BRANCH"),
|
|
30
42
|
timeout: overrides?.timeout || 3e4,
|
|
31
43
|
parentOrigin: overrides?.parentOrigin,
|
|
32
44
|
disableAutoAuth: overrides?.disableAutoAuth
|
|
@@ -66,35 +78,50 @@ function createConfig(overrides) {
|
|
|
66
78
|
* ```
|
|
67
79
|
*/
|
|
68
80
|
async function createConfigAsync(overrides) {
|
|
69
|
-
const { isInIframe: isInIframe$1
|
|
70
|
-
let baseUrl = overrides?.baseUrl ||
|
|
71
|
-
let apiKey = overrides?.apiKey ||
|
|
72
|
-
let projectId = overrides?.projectId ||
|
|
81
|
+
const { isInIframe: isInIframe$1 } = await import("./postMessage-D5wWgwcO.mjs");
|
|
82
|
+
let baseUrl = overrides?.baseUrl || getEnvVar("OXY_URL");
|
|
83
|
+
let apiKey = overrides?.apiKey || getEnvVar("OXY_API_KEY");
|
|
84
|
+
let projectId = overrides?.projectId || getEnvVar("OXY_PROJECT_ID");
|
|
73
85
|
const disableAutoAuth = overrides?.disableAutoAuth ?? false;
|
|
74
86
|
const parentOrigin = overrides?.parentOrigin;
|
|
75
|
-
if (!disableAutoAuth && isInIframe$1() && !apiKey) if (!parentOrigin)
|
|
76
|
-
else
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
apiKey = authResult.apiKey;
|
|
82
|
-
if (authResult.projectId) projectId = authResult.projectId;
|
|
83
|
-
if (authResult.baseUrl) baseUrl = authResult.baseUrl;
|
|
84
|
-
console.log("[Oxy SDK] Successfully authenticated via postMessage");
|
|
85
|
-
} catch (error) {
|
|
87
|
+
if (!disableAutoAuth && isInIframe$1() && !apiKey) if (!parentOrigin) logWarningAboutMissingParentOrigin();
|
|
88
|
+
else apiKey = await attemptPostMessageAuth(parentOrigin, overrides?.timeout || 5e3, apiKey, projectId, baseUrl).then((result) => {
|
|
89
|
+
if (result.projectId) projectId = result.projectId;
|
|
90
|
+
if (result.baseUrl) baseUrl = result.baseUrl;
|
|
91
|
+
return result.apiKey;
|
|
92
|
+
}).catch((error) => {
|
|
86
93
|
console.error("[Oxy SDK] Failed to authenticate via postMessage:", error.message);
|
|
87
|
-
|
|
94
|
+
return apiKey;
|
|
95
|
+
});
|
|
96
|
+
return createFinalConfig(baseUrl, apiKey, projectId, overrides);
|
|
97
|
+
}
|
|
98
|
+
function logWarningAboutMissingParentOrigin() {
|
|
99
|
+
console.warn("[Oxy SDK] Running in iframe without API key and no parentOrigin specified. PostMessage authentication will be skipped. Provide parentOrigin config to enable automatic authentication.");
|
|
100
|
+
}
|
|
101
|
+
async function attemptPostMessageAuth(parentOrigin, timeout, currentApiKey, currentProjectId, currentBaseUrl) {
|
|
102
|
+
const { requestAuthFromParent: requestAuthFromParent$1 } = await import("./postMessage-D5wWgwcO.mjs");
|
|
103
|
+
const authResult = await requestAuthFromParent$1({
|
|
104
|
+
parentOrigin,
|
|
105
|
+
timeout
|
|
106
|
+
});
|
|
107
|
+
console.log("[Oxy SDK] Successfully authenticated via postMessage");
|
|
108
|
+
return {
|
|
109
|
+
apiKey: authResult.apiKey || currentApiKey,
|
|
110
|
+
projectId: authResult.projectId || currentProjectId,
|
|
111
|
+
baseUrl: authResult.baseUrl || currentBaseUrl
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function createFinalConfig(baseUrl, apiKey, projectId, overrides) {
|
|
88
115
|
if (!baseUrl) throw new Error("OXY_URL environment variable or baseUrl config is required");
|
|
89
116
|
if (!projectId) throw new Error("OXY_PROJECT_ID environment variable or projectId config is required");
|
|
90
117
|
return {
|
|
91
118
|
baseUrl: baseUrl.replace(/\/$/, ""),
|
|
92
119
|
apiKey,
|
|
93
120
|
projectId,
|
|
94
|
-
branch: overrides?.branch ||
|
|
121
|
+
branch: overrides?.branch || getEnvVar("OXY_BRANCH"),
|
|
95
122
|
timeout: overrides?.timeout || 3e4,
|
|
96
|
-
parentOrigin,
|
|
97
|
-
disableAutoAuth
|
|
123
|
+
parentOrigin: overrides?.parentOrigin,
|
|
124
|
+
disableAutoAuth: overrides?.disableAutoAuth
|
|
98
125
|
};
|
|
99
126
|
}
|
|
100
127
|
|
|
@@ -102,6 +129,15 @@ async function createConfigAsync(overrides) {
|
|
|
102
129
|
//#region src/parquet.ts
|
|
103
130
|
let dbInstance = null;
|
|
104
131
|
let connection = null;
|
|
132
|
+
let operationQueue = Promise.resolve();
|
|
133
|
+
/**
|
|
134
|
+
* Enqueue an operation to prevent race conditions on shared DuckDB instance
|
|
135
|
+
*/
|
|
136
|
+
function enqueueOperation(operation) {
|
|
137
|
+
const currentOperation = operationQueue.then(operation, operation);
|
|
138
|
+
operationQueue = currentOperation.then(() => {}, () => {});
|
|
139
|
+
return currentOperation;
|
|
140
|
+
}
|
|
105
141
|
/**
|
|
106
142
|
* Initialize DuckDB-WASM instance
|
|
107
143
|
*/
|
|
@@ -126,36 +162,74 @@ async function getConnection() {
|
|
|
126
162
|
return connection;
|
|
127
163
|
}
|
|
128
164
|
/**
|
|
129
|
-
* ParquetReader provides methods to read and query Parquet files
|
|
165
|
+
* ParquetReader provides methods to read and query Parquet files.
|
|
166
|
+
* Supports registering multiple Parquet files with different table names.
|
|
130
167
|
*/
|
|
131
168
|
var ParquetReader = class {
|
|
132
|
-
constructor(
|
|
133
|
-
this.
|
|
134
|
-
|
|
169
|
+
constructor() {
|
|
170
|
+
this.tableMap = /* @__PURE__ */ new Map();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Generate a unique internal table name to prevent conflicts
|
|
174
|
+
*/
|
|
175
|
+
generateInternalTableName(tableName) {
|
|
176
|
+
return `${tableName}_${`${Date.now()}_${Math.random().toString(36).substring(2, 9)}`}`;
|
|
135
177
|
}
|
|
136
178
|
/**
|
|
137
|
-
* Register a Parquet file from a Blob
|
|
179
|
+
* Register a Parquet file from a Blob with a specific table name
|
|
138
180
|
*
|
|
139
181
|
* @param blob - Parquet file as Blob
|
|
182
|
+
* @param tableName - Name to use for the table in queries (required)
|
|
140
183
|
*
|
|
141
184
|
* @example
|
|
142
185
|
* ```typescript
|
|
143
186
|
* const blob = await client.getFile('data/sales.parquet');
|
|
144
|
-
* const reader = new ParquetReader(
|
|
145
|
-
* await reader.registerParquet(blob);
|
|
187
|
+
* const reader = new ParquetReader();
|
|
188
|
+
* await reader.registerParquet(blob, 'sales');
|
|
189
|
+
* ```
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* // Register multiple files
|
|
194
|
+
* const reader = new ParquetReader();
|
|
195
|
+
* await reader.registerParquet(salesBlob, 'sales');
|
|
196
|
+
* await reader.registerParquet(customersBlob, 'customers');
|
|
197
|
+
* const result = await reader.query('SELECT * FROM sales JOIN customers ON sales.customer_id = customers.id');
|
|
146
198
|
* ```
|
|
147
199
|
*/
|
|
148
|
-
async registerParquet(blob) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
await
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
200
|
+
async registerParquet(blob, tableName) {
|
|
201
|
+
const internalTableName = this.generateInternalTableName(tableName);
|
|
202
|
+
await enqueueOperation(async () => {
|
|
203
|
+
const conn = await getConnection();
|
|
204
|
+
const db = await initializeDuckDB();
|
|
205
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
206
|
+
const uint8Array = new Uint8Array(arrayBuffer);
|
|
207
|
+
await db.registerFileBuffer(`${internalTableName}.parquet`, uint8Array);
|
|
208
|
+
try {
|
|
209
|
+
await conn.query(`DROP TABLE IF EXISTS ${internalTableName}`);
|
|
210
|
+
} catch {}
|
|
211
|
+
await conn.query(`CREATE TABLE ${internalTableName} AS SELECT * FROM '${internalTableName}.parquet'`);
|
|
212
|
+
this.tableMap.set(tableName, internalTableName);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Register multiple Parquet files at once
|
|
217
|
+
*
|
|
218
|
+
* @param files - Array of objects containing blob and tableName
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```typescript
|
|
222
|
+
* const reader = new ParquetReader();
|
|
223
|
+
* await reader.registerMultipleParquet([
|
|
224
|
+
* { blob: salesBlob, tableName: 'sales' },
|
|
225
|
+
* { blob: customersBlob, tableName: 'customers' },
|
|
226
|
+
* { blob: productsBlob, tableName: 'products' }
|
|
227
|
+
* ]);
|
|
228
|
+
* const result = await reader.query('SELECT * FROM sales JOIN customers ON sales.customer_id = customers.id');
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
async registerMultipleParquet(files) {
|
|
232
|
+
for (const file of files) await this.registerParquet(file.blob, file.tableName);
|
|
159
233
|
}
|
|
160
234
|
/**
|
|
161
235
|
* Execute a SQL query against the registered Parquet data
|
|
@@ -169,107 +243,129 @@ var ParquetReader = class {
|
|
|
169
243
|
* console.log(result.columns);
|
|
170
244
|
* console.log(result.rows);
|
|
171
245
|
* ```
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```typescript
|
|
249
|
+
* // Query multiple tables
|
|
250
|
+
* await reader.registerParquet(salesBlob, 'sales');
|
|
251
|
+
* await reader.registerParquet(customersBlob, 'customers');
|
|
252
|
+
* const result = await reader.query(`
|
|
253
|
+
* SELECT s.*, c.name
|
|
254
|
+
* FROM sales s
|
|
255
|
+
* JOIN customers c ON s.customer_id = c.id
|
|
256
|
+
* `);
|
|
257
|
+
* ```
|
|
172
258
|
*/
|
|
173
259
|
async query(sql) {
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
260
|
+
if (this.tableMap.size === 0) throw new Error("No Parquet files registered. Call registerParquet() first.");
|
|
261
|
+
return enqueueOperation(async () => {
|
|
262
|
+
const conn = await getConnection();
|
|
263
|
+
let rewrittenSql = sql;
|
|
264
|
+
for (const [userTableName, internalTableName] of this.tableMap.entries()) rewrittenSql = rewrittenSql.replace(new RegExp(`\\b${userTableName}\\b`, "g"), internalTableName);
|
|
265
|
+
const result = await conn.query(rewrittenSql);
|
|
266
|
+
const columns = result.schema.fields.map((field) => field.name);
|
|
267
|
+
const rows = [];
|
|
268
|
+
for (let i = 0; i < result.numRows; i++) {
|
|
269
|
+
const row = [];
|
|
270
|
+
for (let j = 0; j < result.numCols; j++) {
|
|
271
|
+
const col = result.getChildAt(j);
|
|
272
|
+
row.push(col?.get(i));
|
|
273
|
+
}
|
|
274
|
+
rows.push(row);
|
|
183
275
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
276
|
+
return {
|
|
277
|
+
columns,
|
|
278
|
+
rows,
|
|
279
|
+
rowCount: result.numRows
|
|
280
|
+
};
|
|
281
|
+
});
|
|
191
282
|
}
|
|
192
283
|
/**
|
|
193
|
-
* Get all data from
|
|
284
|
+
* Get all data from a registered table
|
|
194
285
|
*
|
|
286
|
+
* @param tableName - Name of the table to query
|
|
195
287
|
* @param limit - Maximum number of rows to return (default: all)
|
|
196
288
|
* @returns Query result
|
|
197
289
|
*
|
|
198
290
|
* @example
|
|
199
291
|
* ```typescript
|
|
200
|
-
* const allData = await reader.getAll();
|
|
201
|
-
* const first100 = await reader.getAll(100);
|
|
292
|
+
* const allData = await reader.getAll('sales');
|
|
293
|
+
* const first100 = await reader.getAll('sales', 100);
|
|
202
294
|
* ```
|
|
203
295
|
*/
|
|
204
|
-
async getAll(limit) {
|
|
296
|
+
async getAll(tableName, limit) {
|
|
205
297
|
const limitClause = limit ? ` LIMIT ${limit}` : "";
|
|
206
|
-
return this.query(`SELECT * FROM ${
|
|
298
|
+
return this.query(`SELECT * FROM ${tableName}${limitClause}`);
|
|
207
299
|
}
|
|
208
300
|
/**
|
|
209
301
|
* Get table schema information
|
|
210
302
|
*
|
|
303
|
+
* @param tableName - Name of the table to describe
|
|
211
304
|
* @returns Schema information
|
|
212
305
|
*
|
|
213
306
|
* @example
|
|
214
307
|
* ```typescript
|
|
215
|
-
* const schema = await reader.getSchema();
|
|
308
|
+
* const schema = await reader.getSchema('sales');
|
|
216
309
|
* console.log(schema.columns); // ['id', 'name', 'sales']
|
|
217
310
|
* console.log(schema.rows); // [['id', 'INTEGER'], ['name', 'VARCHAR'], ...]
|
|
218
311
|
* ```
|
|
219
312
|
*/
|
|
220
|
-
async getSchema() {
|
|
221
|
-
return this.query(`DESCRIBE ${
|
|
313
|
+
async getSchema(tableName) {
|
|
314
|
+
return this.query(`DESCRIBE ${tableName}`);
|
|
222
315
|
}
|
|
223
316
|
/**
|
|
224
|
-
* Get row count
|
|
317
|
+
* Get row count for a table
|
|
225
318
|
*
|
|
319
|
+
* @param tableName - Name of the table to count
|
|
226
320
|
* @returns Number of rows in the table
|
|
227
321
|
*
|
|
228
322
|
* @example
|
|
229
323
|
* ```typescript
|
|
230
|
-
* const count = await reader.count();
|
|
324
|
+
* const count = await reader.count('sales');
|
|
231
325
|
* console.log(`Total rows: ${count}`);
|
|
232
326
|
* ```
|
|
233
327
|
*/
|
|
234
|
-
async count() {
|
|
235
|
-
return (await this.query(`SELECT COUNT(*) as count FROM ${
|
|
328
|
+
async count(tableName) {
|
|
329
|
+
return (await this.query(`SELECT COUNT(*) as count FROM ${tableName}`)).rows[0][0];
|
|
236
330
|
}
|
|
237
331
|
/**
|
|
238
|
-
* Close and cleanup resources
|
|
332
|
+
* Close and cleanup all registered resources
|
|
239
333
|
*/
|
|
240
334
|
async close() {
|
|
241
|
-
if (this.
|
|
335
|
+
if (this.tableMap.size > 0) await enqueueOperation(async () => {
|
|
242
336
|
const conn = await getConnection();
|
|
243
337
|
const db = await initializeDuckDB();
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
338
|
+
for (const [, internalTableName] of this.tableMap.entries()) {
|
|
339
|
+
try {
|
|
340
|
+
await conn.query(`DROP TABLE IF EXISTS ${internalTableName}`);
|
|
341
|
+
} catch {}
|
|
342
|
+
try {
|
|
343
|
+
await db.dropFile(`${internalTableName}.parquet`);
|
|
344
|
+
} catch {}
|
|
345
|
+
}
|
|
346
|
+
this.tableMap.clear();
|
|
347
|
+
});
|
|
252
348
|
}
|
|
253
349
|
};
|
|
254
350
|
/**
|
|
255
351
|
* Helper function to quickly read a Parquet blob and execute a query
|
|
256
352
|
*
|
|
257
353
|
* @param blob - Parquet file as Blob
|
|
258
|
-
* @param
|
|
354
|
+
* @param tableName - Name to use for the table in queries (default: 'data')
|
|
355
|
+
* @param sql - SQL query to execute (optional, defaults to SELECT * FROM tableName)
|
|
259
356
|
* @returns Query result
|
|
260
357
|
*
|
|
261
358
|
* @example
|
|
262
359
|
* ```typescript
|
|
263
360
|
* const blob = await client.getFile('data/sales.parquet');
|
|
264
|
-
* const result = await queryParquet(blob, 'SELECT product, SUM(amount) as total FROM
|
|
361
|
+
* const result = await queryParquet(blob, 'sales', 'SELECT product, SUM(amount) as total FROM sales GROUP BY product');
|
|
265
362
|
* console.log(result);
|
|
266
363
|
* ```
|
|
267
364
|
*/
|
|
268
|
-
async function queryParquet(blob, sql) {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const query = sql || `SELECT * FROM ${uniqueId}`;
|
|
365
|
+
async function queryParquet(blob, tableName = "data", sql) {
|
|
366
|
+
const reader = new ParquetReader();
|
|
367
|
+
await reader.registerParquet(blob, tableName);
|
|
368
|
+
const query = sql || `SELECT * FROM ${tableName}`;
|
|
273
369
|
const result = await reader.query(query);
|
|
274
370
|
await reader.close();
|
|
275
371
|
return result;
|
|
@@ -278,20 +374,21 @@ async function queryParquet(blob, sql) {
|
|
|
278
374
|
* Helper function to read Parquet file and get all data
|
|
279
375
|
*
|
|
280
376
|
* @param blob - Parquet file as Blob
|
|
377
|
+
* @param tableName - Name to use for the table (default: 'data')
|
|
281
378
|
* @param limit - Maximum number of rows (optional)
|
|
282
379
|
* @returns Query result
|
|
283
380
|
*
|
|
284
381
|
* @example
|
|
285
382
|
* ```typescript
|
|
286
383
|
* const blob = await client.getFile('data/sales.parquet');
|
|
287
|
-
* const data = await readParquet(blob, 1000);
|
|
384
|
+
* const data = await readParquet(blob, 'sales', 1000);
|
|
288
385
|
* console.log(`Loaded ${data.rowCount} rows`);
|
|
289
386
|
* ```
|
|
290
387
|
*/
|
|
291
|
-
async function readParquet(blob, limit) {
|
|
292
|
-
const reader = new ParquetReader(
|
|
293
|
-
await reader.registerParquet(blob);
|
|
294
|
-
const result = await reader.getAll(limit);
|
|
388
|
+
async function readParquet(blob, tableName = "data", limit) {
|
|
389
|
+
const reader = new ParquetReader();
|
|
390
|
+
await reader.registerParquet(blob, tableName);
|
|
391
|
+
const result = await reader.getAll(tableName, limit);
|
|
295
392
|
await reader.close();
|
|
296
393
|
return result;
|
|
297
394
|
}
|
|
@@ -370,7 +467,7 @@ var OxyClient = class OxyClient {
|
|
|
370
467
|
return response.json();
|
|
371
468
|
} catch (error) {
|
|
372
469
|
clearTimeout(timeoutId);
|
|
373
|
-
if (error.name === "AbortError") throw new Error(`Request timeout after ${this.config.timeout || 3e4}ms`);
|
|
470
|
+
if (error instanceof Error && error.name === "AbortError") throw new Error(`Request timeout after ${this.config.timeout || 3e4}ms`);
|
|
374
471
|
throw error;
|
|
375
472
|
}
|
|
376
473
|
}
|
|
@@ -491,7 +588,7 @@ var OxyClient = class OxyClient {
|
|
|
491
588
|
async getFile(filePath) {
|
|
492
589
|
const pathb64 = this.encodePathBase64(filePath);
|
|
493
590
|
const query = this.buildQueryParams();
|
|
494
|
-
return this.request(`/${this.config.projectId}/app/file/${pathb64}${query}`, { headers: {
|
|
591
|
+
return this.request(`/${this.config.projectId}/app/file/${pathb64}${query}`, { headers: { Accept: "application/octet-stream" } });
|
|
495
592
|
}
|
|
496
593
|
/**
|
|
497
594
|
* Gets a file URL for direct browser access
|
|
@@ -531,7 +628,7 @@ var OxyClient = class OxyClient {
|
|
|
531
628
|
* ```
|
|
532
629
|
*/
|
|
533
630
|
async getTableData(filePath, limit = 100) {
|
|
534
|
-
const result = await readParquet(await this.getFile(filePath), limit);
|
|
631
|
+
const result = await readParquet(await this.getFile(filePath), "data", limit);
|
|
535
632
|
return {
|
|
536
633
|
columns: result.columns,
|
|
537
634
|
rows: result.rows,
|
|
@@ -541,5 +638,400 @@ var OxyClient = class OxyClient {
|
|
|
541
638
|
};
|
|
542
639
|
|
|
543
640
|
//#endregion
|
|
544
|
-
|
|
641
|
+
//#region src/sdk.ts
|
|
642
|
+
/**
|
|
643
|
+
* OxySDK provides a unified interface for fetching data from Oxy and querying it with SQL.
|
|
644
|
+
* It combines OxyClient (for API calls) and ParquetReader (for SQL queries) into a single,
|
|
645
|
+
* easy-to-use interface.
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* ```typescript
|
|
649
|
+
* // Create SDK instance
|
|
650
|
+
* const sdk = new OxySDK({ apiKey: 'your-key', projectId: 'your-project' });
|
|
651
|
+
*
|
|
652
|
+
* // Load a parquet file and query it
|
|
653
|
+
* await sdk.loadFile('data/sales.parquet', 'sales');
|
|
654
|
+
* const result = await sdk.query('SELECT * FROM sales WHERE amount > 1000');
|
|
655
|
+
* console.log(result.rows);
|
|
656
|
+
*
|
|
657
|
+
* // Clean up when done
|
|
658
|
+
* await sdk.close();
|
|
659
|
+
* ```
|
|
660
|
+
*/
|
|
661
|
+
var OxySDK = class OxySDK {
|
|
662
|
+
constructor(config) {
|
|
663
|
+
this.client = new OxyClient(config);
|
|
664
|
+
this.reader = new ParquetReader();
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Creates an OxySDK instance asynchronously with support for postMessage authentication
|
|
668
|
+
*
|
|
669
|
+
* @param config - Optional configuration overrides
|
|
670
|
+
* @returns Promise resolving to OxySDK instance
|
|
671
|
+
*
|
|
672
|
+
* @example
|
|
673
|
+
* ```typescript
|
|
674
|
+
* // In an iframe - automatic postMessage auth
|
|
675
|
+
* const sdk = await OxySDK.create({
|
|
676
|
+
* parentOrigin: 'https://app.example.com',
|
|
677
|
+
* projectId: 'my-project-id'
|
|
678
|
+
* });
|
|
679
|
+
* ```
|
|
680
|
+
*/
|
|
681
|
+
static async create(config) {
|
|
682
|
+
return new OxySDK(await createConfigAsync(config));
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Load a Parquet file from Oxy and register it for SQL queries
|
|
686
|
+
*
|
|
687
|
+
* @param filePath - Path to the parquet file in the app state directory
|
|
688
|
+
* @param tableName - Name to use for the table in SQL queries
|
|
689
|
+
*
|
|
690
|
+
* @example
|
|
691
|
+
* ```typescript
|
|
692
|
+
* await sdk.loadFile('data/sales.parquet', 'sales');
|
|
693
|
+
* await sdk.loadFile('data/customers.parquet', 'customers');
|
|
694
|
+
*
|
|
695
|
+
* const result = await sdk.query(`
|
|
696
|
+
* SELECT s.*, c.name
|
|
697
|
+
* FROM sales s
|
|
698
|
+
* JOIN customers c ON s.customer_id = c.id
|
|
699
|
+
* `);
|
|
700
|
+
* ```
|
|
701
|
+
*/
|
|
702
|
+
async loadFile(filePath, tableName) {
|
|
703
|
+
const blob = await this.client.getFile(filePath);
|
|
704
|
+
await this.reader.registerParquet(blob, tableName);
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Load multiple Parquet files at once
|
|
708
|
+
*
|
|
709
|
+
* @param files - Array of file paths and table names
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* ```typescript
|
|
713
|
+
* await sdk.loadFiles([
|
|
714
|
+
* { filePath: 'data/sales.parquet', tableName: 'sales' },
|
|
715
|
+
* { filePath: 'data/customers.parquet', tableName: 'customers' },
|
|
716
|
+
* { filePath: 'data/products.parquet', tableName: 'products' }
|
|
717
|
+
* ]);
|
|
718
|
+
*
|
|
719
|
+
* const result = await sdk.query('SELECT * FROM sales');
|
|
720
|
+
* ```
|
|
721
|
+
*/
|
|
722
|
+
async loadFiles(files) {
|
|
723
|
+
for (const file of files) await this.loadFile(file.filePath, file.tableName);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Load all data from an app's data container
|
|
727
|
+
*
|
|
728
|
+
* This fetches the app's data and registers all parquet files using their container keys as table names.
|
|
729
|
+
*
|
|
730
|
+
* @param appPath - Path to the app file
|
|
731
|
+
* @returns DataContainer with file references
|
|
732
|
+
*
|
|
733
|
+
* @example
|
|
734
|
+
* ```typescript
|
|
735
|
+
* // If app has data: { sales: { file_path: 'data/sales.parquet' } }
|
|
736
|
+
* const data = await sdk.loadAppData('dashboard.app.yml');
|
|
737
|
+
* // Now you can query the 'sales' table
|
|
738
|
+
* const result = await sdk.query('SELECT * FROM sales LIMIT 10');
|
|
739
|
+
* ```
|
|
740
|
+
*/
|
|
741
|
+
async loadAppData(appPath) {
|
|
742
|
+
const appDataResponse = await this.client.getAppData(appPath);
|
|
743
|
+
if (appDataResponse.error) throw new Error(`Failed to load app data: ${appDataResponse.error}`);
|
|
744
|
+
if (!appDataResponse.data) return null;
|
|
745
|
+
const loadPromises = Object.entries(appDataResponse.data).map(async ([tableName, fileRef]) => {
|
|
746
|
+
await this.loadFile(fileRef.file_path, tableName);
|
|
747
|
+
});
|
|
748
|
+
await Promise.all(loadPromises);
|
|
749
|
+
return appDataResponse.data;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Execute a SQL query against loaded data
|
|
753
|
+
*
|
|
754
|
+
* @param sql - SQL query to execute
|
|
755
|
+
* @returns Query result with columns and rows
|
|
756
|
+
*
|
|
757
|
+
* @example
|
|
758
|
+
* ```typescript
|
|
759
|
+
* await sdk.loadFile('data/sales.parquet', 'sales');
|
|
760
|
+
*
|
|
761
|
+
* const result = await sdk.query('SELECT product, SUM(amount) as total FROM sales GROUP BY product');
|
|
762
|
+
* console.log(result.columns); // ['product', 'total']
|
|
763
|
+
* console.log(result.rows); // [['Product A', 1000], ['Product B', 2000]]
|
|
764
|
+
* console.log(result.rowCount); // 2
|
|
765
|
+
* ```
|
|
766
|
+
*/
|
|
767
|
+
async query(sql) {
|
|
768
|
+
return this.reader.query(sql);
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Get all data from a loaded table
|
|
772
|
+
*
|
|
773
|
+
* @param tableName - Name of the table
|
|
774
|
+
* @param limit - Maximum number of rows (optional)
|
|
775
|
+
* @returns Query result
|
|
776
|
+
*
|
|
777
|
+
* @example
|
|
778
|
+
* ```typescript
|
|
779
|
+
* await sdk.loadFile('data/sales.parquet', 'sales');
|
|
780
|
+
* const allData = await sdk.getAll('sales');
|
|
781
|
+
* const first100 = await sdk.getAll('sales', 100);
|
|
782
|
+
* ```
|
|
783
|
+
*/
|
|
784
|
+
async getAll(tableName, limit) {
|
|
785
|
+
return this.reader.getAll(tableName, limit);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Get schema information for a loaded table
|
|
789
|
+
*
|
|
790
|
+
* @param tableName - Name of the table
|
|
791
|
+
* @returns Schema information
|
|
792
|
+
*
|
|
793
|
+
* @example
|
|
794
|
+
* ```typescript
|
|
795
|
+
* await sdk.loadFile('data/sales.parquet', 'sales');
|
|
796
|
+
* const schema = await sdk.getSchema('sales');
|
|
797
|
+
* console.log(schema.columns); // ['column_name', 'column_type', ...]
|
|
798
|
+
* console.log(schema.rows); // [['id', 'INTEGER'], ['name', 'VARCHAR'], ...]
|
|
799
|
+
* ```
|
|
800
|
+
*/
|
|
801
|
+
async getSchema(tableName) {
|
|
802
|
+
return this.reader.getSchema(tableName);
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Get row count for a loaded table
|
|
806
|
+
*
|
|
807
|
+
* @param tableName - Name of the table
|
|
808
|
+
* @returns Number of rows
|
|
809
|
+
*
|
|
810
|
+
* @example
|
|
811
|
+
* ```typescript
|
|
812
|
+
* await sdk.loadFile('data/sales.parquet', 'sales');
|
|
813
|
+
* const count = await sdk.count('sales');
|
|
814
|
+
* console.log(`Total rows: ${count}`);
|
|
815
|
+
* ```
|
|
816
|
+
*/
|
|
817
|
+
async count(tableName) {
|
|
818
|
+
return this.reader.count(tableName);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Get direct access to the underlying OxyClient
|
|
822
|
+
*
|
|
823
|
+
* Useful for advanced operations like listing apps, getting displays, etc.
|
|
824
|
+
*
|
|
825
|
+
* @returns The OxyClient instance
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* ```typescript
|
|
829
|
+
* const apps = await sdk.getClient().listApps();
|
|
830
|
+
* const displays = await sdk.getClient().getDisplays('my-app.app.yml');
|
|
831
|
+
* ```
|
|
832
|
+
*/
|
|
833
|
+
getClient() {
|
|
834
|
+
return this.client;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Get direct access to the underlying ParquetReader
|
|
838
|
+
*
|
|
839
|
+
* Useful for advanced operations like registering blobs directly.
|
|
840
|
+
*
|
|
841
|
+
* @returns The ParquetReader instance
|
|
842
|
+
*
|
|
843
|
+
* @example
|
|
844
|
+
* ```typescript
|
|
845
|
+
* const myBlob = new Blob([parquetData]);
|
|
846
|
+
* await sdk.getReader().registerParquet(myBlob, 'mydata');
|
|
847
|
+
* ```
|
|
848
|
+
*/
|
|
849
|
+
getReader() {
|
|
850
|
+
return this.reader;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Close and cleanup all resources
|
|
854
|
+
*
|
|
855
|
+
* This clears all loaded data and releases resources. Call this when you're done with the SDK.
|
|
856
|
+
*
|
|
857
|
+
* @example
|
|
858
|
+
* ```typescript
|
|
859
|
+
* const sdk = new OxySDK({ apiKey: 'key', projectId: 'project' });
|
|
860
|
+
* await sdk.loadFile('data/sales.parquet', 'sales');
|
|
861
|
+
* const result = await sdk.query('SELECT * FROM sales');
|
|
862
|
+
* await sdk.close(); // Clean up
|
|
863
|
+
* ```
|
|
864
|
+
*/
|
|
865
|
+
async close() {
|
|
866
|
+
await this.reader.close();
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
//#endregion
|
|
871
|
+
//#region src/react.tsx
|
|
872
|
+
/**
|
|
873
|
+
* React context for OxySDK
|
|
874
|
+
*/
|
|
875
|
+
const OxyContext = createContext(void 0);
|
|
876
|
+
/**
|
|
877
|
+
* Provider component that initializes and provides OxySDK to child components
|
|
878
|
+
*
|
|
879
|
+
* @example
|
|
880
|
+
* ```tsx
|
|
881
|
+
* // Synchronous initialization with config
|
|
882
|
+
* function App() {
|
|
883
|
+
* return (
|
|
884
|
+
* <OxyProvider config={{
|
|
885
|
+
* apiKey: 'your-key',
|
|
886
|
+
* projectId: 'your-project',
|
|
887
|
+
* baseUrl: 'https://api.oxy.tech'
|
|
888
|
+
* }}>
|
|
889
|
+
* <Dashboard />
|
|
890
|
+
* </OxyProvider>
|
|
891
|
+
* );
|
|
892
|
+
* }
|
|
893
|
+
* ```
|
|
894
|
+
*
|
|
895
|
+
* @example
|
|
896
|
+
* ```tsx
|
|
897
|
+
* // Async initialization (for iframe/postMessage auth)
|
|
898
|
+
* function App() {
|
|
899
|
+
* return (
|
|
900
|
+
* <OxyProvider
|
|
901
|
+
* useAsync
|
|
902
|
+
* config={{ parentOrigin: 'https://app.example.com' }}
|
|
903
|
+
* >
|
|
904
|
+
* <Dashboard />
|
|
905
|
+
* </OxyProvider>
|
|
906
|
+
* );
|
|
907
|
+
* }
|
|
908
|
+
* ```
|
|
909
|
+
*
|
|
910
|
+
* @example
|
|
911
|
+
* ```tsx
|
|
912
|
+
* // With environment variables
|
|
913
|
+
* import { createConfig } from '@oxy/sdk';
|
|
914
|
+
*
|
|
915
|
+
* function App() {
|
|
916
|
+
* return (
|
|
917
|
+
* <OxyProvider config={createConfig()}>
|
|
918
|
+
* <Dashboard />
|
|
919
|
+
* </OxyProvider>
|
|
920
|
+
* );
|
|
921
|
+
* }
|
|
922
|
+
* ```
|
|
923
|
+
*/
|
|
924
|
+
function OxyProvider({ children, config, useAsync = false, onReady, onError }) {
|
|
925
|
+
const [sdk, setSdk] = useState(null);
|
|
926
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
927
|
+
const [error, setError] = useState(null);
|
|
928
|
+
useEffect(() => {
|
|
929
|
+
let mounted = true;
|
|
930
|
+
let sdkInstance = null;
|
|
931
|
+
async function initializeSDK() {
|
|
932
|
+
try {
|
|
933
|
+
setIsLoading(true);
|
|
934
|
+
setError(null);
|
|
935
|
+
if (useAsync) sdkInstance = await OxySDK.create(config);
|
|
936
|
+
else {
|
|
937
|
+
if (!config) throw new Error("Config is required when useAsync is false. Either provide config or set useAsync=true.");
|
|
938
|
+
sdkInstance = new OxySDK(config);
|
|
939
|
+
}
|
|
940
|
+
if (mounted) {
|
|
941
|
+
setSdk(sdkInstance);
|
|
942
|
+
setIsLoading(false);
|
|
943
|
+
onReady?.(sdkInstance);
|
|
944
|
+
}
|
|
945
|
+
} catch (err) {
|
|
946
|
+
const error$1 = err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to initialize SDK");
|
|
947
|
+
if (mounted) {
|
|
948
|
+
setError(error$1);
|
|
949
|
+
setIsLoading(false);
|
|
950
|
+
onError?.(error$1);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
initializeSDK();
|
|
955
|
+
return () => {
|
|
956
|
+
mounted = false;
|
|
957
|
+
if (sdkInstance) sdkInstance.close().catch(console.error);
|
|
958
|
+
};
|
|
959
|
+
}, [
|
|
960
|
+
config,
|
|
961
|
+
useAsync,
|
|
962
|
+
onReady,
|
|
963
|
+
onError
|
|
964
|
+
]);
|
|
965
|
+
return /* @__PURE__ */ React.createElement(OxyContext.Provider, { value: {
|
|
966
|
+
sdk,
|
|
967
|
+
isLoading,
|
|
968
|
+
error
|
|
969
|
+
} }, children);
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Hook to access OxySDK from child components
|
|
973
|
+
*
|
|
974
|
+
* @throws {Error} If used outside of OxyProvider
|
|
975
|
+
* @returns {OxyContextValue} The SDK instance, loading state, and error
|
|
976
|
+
*
|
|
977
|
+
* @example
|
|
978
|
+
* ```tsx
|
|
979
|
+
* function Dashboard() {
|
|
980
|
+
* const { sdk, isLoading, error } = useOxy();
|
|
981
|
+
*
|
|
982
|
+
* useEffect(() => {
|
|
983
|
+
* if (sdk) {
|
|
984
|
+
* sdk.loadAppData('dashboard.app.yml')
|
|
985
|
+
* .then(() => sdk.query('SELECT * FROM my_table'))
|
|
986
|
+
* .then(result => console.log(result));
|
|
987
|
+
* }
|
|
988
|
+
* }, [sdk]);
|
|
989
|
+
*
|
|
990
|
+
* if (isLoading) return <div>Loading SDK...</div>;
|
|
991
|
+
* if (error) return <div>Error: {error.message}</div>;
|
|
992
|
+
* if (!sdk) return null;
|
|
993
|
+
*
|
|
994
|
+
* return <div>Dashboard</div>;
|
|
995
|
+
* }
|
|
996
|
+
* ```
|
|
997
|
+
*/
|
|
998
|
+
function useOxy() {
|
|
999
|
+
const context = useContext(OxyContext);
|
|
1000
|
+
if (context === void 0) throw new Error("useOxy must be used within an OxyProvider");
|
|
1001
|
+
return context;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Hook to access OxySDK that throws if not ready
|
|
1005
|
+
*
|
|
1006
|
+
* This is a convenience hook that returns the SDK directly or throws an error if not initialized.
|
|
1007
|
+
* Use this when you know the SDK should be ready.
|
|
1008
|
+
*
|
|
1009
|
+
* @throws {Error} If used outside of OxyProvider or if SDK is not initialized
|
|
1010
|
+
* @returns {OxySDK} The SDK instance
|
|
1011
|
+
*
|
|
1012
|
+
* @example
|
|
1013
|
+
* ```tsx
|
|
1014
|
+
* function DataTable() {
|
|
1015
|
+
* const sdk = useOxySDK();
|
|
1016
|
+
* const [data, setData] = useState(null);
|
|
1017
|
+
*
|
|
1018
|
+
* useEffect(() => {
|
|
1019
|
+
* sdk.loadFile('data.parquet', 'data')
|
|
1020
|
+
* .then(() => sdk.query('SELECT * FROM data LIMIT 100'))
|
|
1021
|
+
* .then(setData);
|
|
1022
|
+
* }, [sdk]);
|
|
1023
|
+
*
|
|
1024
|
+
* return <table>...</table>;
|
|
1025
|
+
* }
|
|
1026
|
+
* ```
|
|
1027
|
+
*/
|
|
1028
|
+
function useOxySDK() {
|
|
1029
|
+
const { sdk, isLoading, error } = useOxy();
|
|
1030
|
+
if (error) throw error;
|
|
1031
|
+
if (isLoading || !sdk) throw new Error("OxySDK is not yet initialized");
|
|
1032
|
+
return sdk;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
//#endregion
|
|
1036
|
+
export { OxyClient, OxyProvider, OxySDK, ParquetReader, PostMessageAuthInvalidOriginError, PostMessageAuthInvalidResponseError, PostMessageAuthNotInIframeError, PostMessageAuthTimeoutError, createConfig, createConfigAsync, initializeDuckDB, isInIframe, queryParquet, readParquet, requestAuthFromParent, useOxy, useOxySDK };
|
|
545
1037
|
//# sourceMappingURL=index.mjs.map
|