@xenosystem/blocks 0.4.1 → 0.5.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.
@@ -1,3 +1,5 @@
1
+ import "../chunk-2KG3PWR4.js";
2
+
1
3
  // src/auth/gate/panel.ts
2
4
  import { createElement } from "react";
3
5
  import { createRoot } from "react-dom/client";
@@ -0,0 +1,17 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __copyProps = (to, from, except, desc) => {
6
+ if (from && typeof from === "object" || typeof from === "function") {
7
+ for (let key of __getOwnPropNames(from))
8
+ if (!__hasOwnProp.call(to, key) && key !== except)
9
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
10
+ }
11
+ return to;
12
+ };
13
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
14
+
15
+ export {
16
+ __reExport
17
+ };
@@ -0,0 +1,215 @@
1
+ // src/data/connectors/executors/sqlBuilder.ts
2
+ import { isFilterGroup } from "@xenosystem/data-core";
3
+ function quoteIdent(name) {
4
+ return `"${String(name).replace(/"/g, '""')}"`;
5
+ }
6
+ function quoteLiteral(value) {
7
+ if (value === null || value === void 0) return "NULL";
8
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
9
+ if (typeof value === "number") {
10
+ return Number.isFinite(value) ? String(value) : "NULL";
11
+ }
12
+ if (Array.isArray(value)) return `(${value.map((v) => quoteLiteral(v)).join(", ")})`;
13
+ if (typeof value === "object") return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
14
+ return `'${String(value).replace(/'/g, "''")}'`;
15
+ }
16
+ function isSafeTableName(name) {
17
+ return /^[A-Za-z_][A-Za-z0-9_]{0,62}$/.test(name);
18
+ }
19
+ function buildSelect(options) {
20
+ const { table, query, limit } = options;
21
+ if (!isSafeTableName(table)) return { error: `"${table}" is not a usable table name.` };
22
+ const known = options.columns ? new Set(options.columns) : null;
23
+ const unknown = (id) => known !== null && !known.has(id);
24
+ const groupBy = query.groupBy ?? [];
25
+ const aggregates = query.aggregates ?? [];
26
+ const projection = [];
27
+ if (groupBy.length > 0 || aggregates.length > 0) {
28
+ for (const level of groupBy) {
29
+ if (unknown(level.columnId)) return { error: `There is no column called "${level.columnId}".` };
30
+ projection.push(quoteIdent(level.columnId));
31
+ }
32
+ for (const aggregate of aggregates) {
33
+ const rendered = renderAggregate(aggregate, unknown);
34
+ if ("error" in rendered) return rendered;
35
+ projection.push(`${rendered.sql} AS ${quoteIdent(aggregate.id)}`);
36
+ }
37
+ } else if (query.select && query.select.length > 0) {
38
+ for (const columnId of query.select) {
39
+ if (unknown(columnId)) return { error: `There is no column called "${columnId}".` };
40
+ projection.push(quoteIdent(columnId));
41
+ }
42
+ }
43
+ const parts = [`SELECT ${projection.length > 0 ? projection.join(", ") : "*"}`];
44
+ parts.push(`FROM ${quoteIdent(table)}`);
45
+ if (query.filters) {
46
+ const where = renderFilterGroup(query.filters, unknown);
47
+ if ("error" in where) return where;
48
+ if (where.sql !== "") parts.push(`WHERE ${where.sql}`);
49
+ }
50
+ if (groupBy.length > 0) {
51
+ parts.push(`GROUP BY ${groupBy.map((level) => quoteIdent(level.columnId)).join(", ")}`);
52
+ }
53
+ if (query.sorts && query.sorts.length > 0) {
54
+ const terms = [];
55
+ for (const sort of query.sorts) {
56
+ const isAlias = aggregates.some((a) => a.id === sort.columnId);
57
+ if (!isAlias && unknown(sort.columnId)) {
58
+ return { error: `There is no column called "${sort.columnId}".` };
59
+ }
60
+ const direction = sort.direction === "desc" ? "DESC" : "ASC";
61
+ const nulls = sort.nulls === "first" ? "NULLS FIRST" : "NULLS LAST";
62
+ terms.push(`${quoteIdent(sort.columnId)} ${direction} ${nulls}`);
63
+ }
64
+ parts.push(`ORDER BY ${terms.join(", ")}`);
65
+ }
66
+ parts.push(`LIMIT ${Math.max(0, Math.floor(limit))}`);
67
+ if (options.offset && options.offset > 0) parts.push(`OFFSET ${Math.floor(options.offset)}`);
68
+ return { sql: parts.join(" ") };
69
+ }
70
+ function renderAggregate(aggregate, unknown) {
71
+ const column = quoteIdent(aggregate.columnId);
72
+ if (aggregate.fn !== "count" && unknown(aggregate.columnId)) {
73
+ return { error: `There is no column called "${aggregate.columnId}".` };
74
+ }
75
+ switch (aggregate.fn) {
76
+ case "count":
77
+ return { sql: "count(*)" };
78
+ case "countDistinct":
79
+ return { sql: `count(DISTINCT ${column})` };
80
+ case "countNumbers":
81
+ return { sql: `count(TRY_CAST(${column} AS DOUBLE))` };
82
+ case "sum":
83
+ return { sql: `sum(${column})` };
84
+ case "avg":
85
+ return { sql: `avg(${column})` };
86
+ case "min":
87
+ return { sql: `min(${column})` };
88
+ case "max":
89
+ return { sql: `max(${column})` };
90
+ case "median":
91
+ case "p50":
92
+ return { sql: `median(${column})` };
93
+ case "p90":
94
+ return { sql: `quantile_cont(${column}, 0.90)` };
95
+ case "p95":
96
+ return { sql: `quantile_cont(${column}, 0.95)` };
97
+ case "p99":
98
+ return { sql: `quantile_cont(${column}, 0.99)` };
99
+ case "stdev":
100
+ return { sql: `stddev_samp(${column})` };
101
+ case "var":
102
+ return { sql: `var_samp(${column})` };
103
+ case "product":
104
+ return { sql: `product(${column})` };
105
+ case "first":
106
+ return { sql: `first(${column})` };
107
+ case "last":
108
+ return { sql: `last(${column})` };
109
+ default:
110
+ return { error: `The aggregate "${String(aggregate.fn)}" is not supported by this source.` };
111
+ }
112
+ }
113
+ function renderFilterGroup(group, unknown = () => false) {
114
+ const parts = [];
115
+ for (const member of group.filters) {
116
+ const rendered = isFilterGroup(member) ? renderFilterGroup(member, unknown) : renderFilter(member, unknown);
117
+ if ("error" in rendered) return rendered;
118
+ if (rendered.sql !== "") parts.push(`(${rendered.sql})`);
119
+ }
120
+ if (parts.length === 0) return { sql: "" };
121
+ const joined = parts.join(group.combinator === "or" ? " OR " : " AND ");
122
+ return { sql: group.negate ? `NOT (${joined})` : joined };
123
+ }
124
+ function renderFilter(filter, unknown) {
125
+ if (unknown(filter.columnId)) return { error: `There is no column called "${filter.columnId}".` };
126
+ const column = quoteIdent(filter.columnId);
127
+ const insensitive = filter.caseInsensitive !== false;
128
+ const text = insensitive ? `lower(CAST(${column} AS VARCHAR))` : `CAST(${column} AS VARCHAR)`;
129
+ const like = (pattern) => `${text} LIKE ${quoteLiteral(insensitive ? pattern.toLowerCase() : pattern)}`;
130
+ const escaped = escapeLikePattern(String(filter.value ?? ""));
131
+ switch (filter.op) {
132
+ case "eq":
133
+ return { sql: filter.value === null ? `${column} IS NULL` : `${column} = ${quoteLiteral(filter.value)}` };
134
+ case "ne":
135
+ return { sql: filter.value === null ? `${column} IS NOT NULL` : `${column} IS DISTINCT FROM ${quoteLiteral(filter.value)}` };
136
+ case "gt":
137
+ return { sql: `${column} > ${quoteLiteral(filter.value)}` };
138
+ case "gte":
139
+ return { sql: `${column} >= ${quoteLiteral(filter.value)}` };
140
+ case "lt":
141
+ return { sql: `${column} < ${quoteLiteral(filter.value)}` };
142
+ case "lte":
143
+ return { sql: `${column} <= ${quoteLiteral(filter.value)}` };
144
+ case "contains":
145
+ return { sql: `${like(`%${escaped}%`)} ESCAPE '\\'` };
146
+ case "notContains":
147
+ return { sql: `NOT (${like(`%${escaped}%`)} ESCAPE '\\')` };
148
+ case "startsWith":
149
+ return { sql: `${like(`${escaped}%`)} ESCAPE '\\'` };
150
+ case "endsWith":
151
+ return { sql: `${like(`%${escaped}`)} ESCAPE '\\'` };
152
+ case "in":
153
+ case "notIn": {
154
+ const values = filter.values ?? [];
155
+ if (values.length === 0) return { sql: filter.op === "in" ? "FALSE" : "TRUE" };
156
+ const list = values.map((v) => quoteLiteral(v)).join(", ");
157
+ return { sql: `${column} ${filter.op === "in" ? "IN" : "NOT IN"} (${list})` };
158
+ }
159
+ case "between": {
160
+ const [low, high] = filter.values ?? [];
161
+ if (low === void 0 || high === void 0) {
162
+ return { error: `The "between" filter on "${filter.columnId}" needs two values.` };
163
+ }
164
+ return { sql: `${column} BETWEEN ${quoteLiteral(low)} AND ${quoteLiteral(high)}` };
165
+ }
166
+ case "isEmpty":
167
+ return { sql: `(${column} IS NULL OR CAST(${column} AS VARCHAR) = '')` };
168
+ case "isNotEmpty":
169
+ return { sql: `(${column} IS NOT NULL AND CAST(${column} AS VARCHAR) <> '')` };
170
+ case "relative": {
171
+ const n = Number(filter.n);
172
+ if (!Number.isFinite(n)) return { error: `The relative filter on "${filter.columnId}" has no window.` };
173
+ const unit = relativeUnit(filter.unit);
174
+ if (!unit) return { error: `The time unit "${String(filter.unit)}" is not supported by this source.` };
175
+ const sign = n < 0 ? "-" : "+";
176
+ const magnitude = Math.abs(Math.floor(n));
177
+ const bound = `now() ${sign} INTERVAL ${magnitude} ${unit}`;
178
+ return { sql: n < 0 ? `${column} BETWEEN ${bound} AND now()` : `${column} BETWEEN now() AND ${bound}` };
179
+ }
180
+ default:
181
+ return { error: `The filter "${String(filter.op)}" is not supported by this source.` };
182
+ }
183
+ }
184
+ function relativeUnit(unit) {
185
+ switch (unit) {
186
+ case "minute":
187
+ return "MINUTE";
188
+ case "hour":
189
+ return "HOUR";
190
+ case "day":
191
+ return "DAY";
192
+ case "week":
193
+ return "WEEK";
194
+ case "month":
195
+ return "MONTH";
196
+ case "quarter":
197
+ return "QUARTER";
198
+ case "year":
199
+ return "YEAR";
200
+ default:
201
+ return null;
202
+ }
203
+ }
204
+ function escapeLikePattern(text) {
205
+ return text.replace(/[\\%_]/g, (char) => `\\${char}`);
206
+ }
207
+
208
+ export {
209
+ quoteIdent,
210
+ quoteLiteral,
211
+ isSafeTableName,
212
+ buildSelect,
213
+ renderFilterGroup,
214
+ escapeLikePattern
215
+ };
@@ -0,0 +1,123 @@
1
+ import { X as XenoSqlEngine } from '../transport-B1cdciP8.js';
2
+ import '@xenosystem/data-core';
3
+
4
+ /**
5
+ * The DuckDB-WASM adapter — the ONLY file in this package that knows DuckDB exists.
6
+ *
7
+ * Ships on the `./duckdb` subpath, imports its engine through `await import(...)`, and declares
8
+ * `@duckdb/duckdb-wasm` as an **optional peer**. A consumer that never opens a SQL source installs
9
+ * nothing and pays nothing; a host that already owns a DuckDB instance implements the three-method
10
+ * {@link XenoSqlEngine} seam against its own and ignores this file entirely.
11
+ *
12
+ * The measured cost of getting that wrong: `duckdb-eh.wasm` is **34 MB raw / ~7.1 MB brotli**, plus
13
+ * a 0.74 MB worker. A static import would put that on every consumer of `@xenosystem/blocks/data`,
14
+ * including the twenty panels that never run SQL. The lucide incident already taught this package
15
+ * that lesson once.
16
+ *
17
+ * ## Four traps, each closed deliberately
18
+ *
19
+ * Every one of these is a documented, currently-open failure mode. They are why this adapter is
20
+ * ~200 lines rather than ~40.
21
+ *
22
+ * 1. **Never `insertArrowTable(tableFromJSON(rows))`.** Arrow's `tableFromJSON` infers
23
+ * `Dictionary<Utf8, Int32>` for string columns, and duckdb-wasm's IPC decoder does not accept
24
+ * dictionary-encoded streams. The failure is *silent*: no error, no rows, and then
25
+ * `Catalog Error: Table with name X does not exist` on the next query. Compounding it,
26
+ * `insertArrowTable` gates on `input instanceof Table`, which is `false` whenever the bundler
27
+ * resolves two copies of `apache-arrow` — and duckdb-wasm pins `^17` while npm serves 21.x.
28
+ * This adapter uses the JSON path instead, which has neither problem.
29
+ *
30
+ * 2. **Never `read_json_auto`.** DuckDB-WASM builds `json`, `parquet`, `icu` and `autocomplete` as
31
+ * `DONT_LINK`, so they are **autoloaded over the network** from `extensions.duckdb.org` at first
32
+ * use. In a sandbox, an offline app or a web export that is a hang or a CORS error — in the one
33
+ * code path whose entire selling point is that it needs no network.
34
+ * `insertJSONFromPath` is implemented in duckdb-wasm's own C++ against Arrow's JSON reader and
35
+ * touches no extension, so it works with zero network. Autoloading is then turned **off**, so a
36
+ * stray `read_parquet` fails loudly instead of hanging.
37
+ *
38
+ * 3. **`StructRowProxy.toJSON()` is shallow.** Nested structs stay proxies and list columns stay
39
+ * Arrow `Vector`s, so a naive `.toJSON()` hands the panel objects it cannot render and
40
+ * `JSON.stringify` cannot serialize. {@link unwrapArrowValue} descends.
41
+ *
42
+ * 4. **BIGINT arrives as `bigint`.** `count(*)` is BIGINT, so the single most common aggregate in
43
+ * existence makes `JSON.stringify` throw `Do not know how to serialize a BigInt`. DECIMAL arrives
44
+ * as a raw `Uint32Array` of 128-bit limbs; timestamps as epoch numbers, not `Date`. The config
45
+ * casts what it can and {@link unwrapArrowValue} handles the rest.
46
+ *
47
+ * @module
48
+ */
49
+
50
+ /**
51
+ * The bundle assets, which the **host** must provide.
52
+ *
53
+ * There is deliberately no jsDelivr default. `getJsDelivrBundles()` would make the "zero
54
+ * capabilities, works offline, survives a web export" promise false the first time it ran, by
55
+ * fetching 34 MB from a third-party CDN. A host that wants the CDN can pass those URLs explicitly
56
+ * and will have made that choice on purpose.
57
+ */
58
+ interface DuckDbBundle {
59
+ /** URL of `duckdb-eh.wasm` (or `duckdb-mvp.wasm`). */
60
+ mainModule: string;
61
+ /** URL of the matching worker script. */
62
+ mainWorker: string;
63
+ /** Only for the cross-origin-isolated bundle. */
64
+ pthreadWorker?: string | null;
65
+ }
66
+ /** Options for {@link createDuckDbEngine}. */
67
+ interface CreateDuckDbEngineOptions {
68
+ /** Where the wasm and worker live. Host-supplied — see {@link DuckDbBundle}. */
69
+ bundle: DuckDbBundle;
70
+ /**
71
+ * Pre-built module, for a host that already imported or vendored duckdb-wasm.
72
+ *
73
+ * Supplying this skips the dynamic import entirely, which is how an Electron host that bundles
74
+ * the package avoids a second copy.
75
+ */
76
+ module?: unknown;
77
+ /** A ready `AsyncDuckDB`. Supplying it skips instantiation altogether. */
78
+ db?: unknown;
79
+ /** Log query text. Off by default — a raw statement can embed user data. */
80
+ debug?: boolean;
81
+ }
82
+ /**
83
+ * Build an {@link XenoSqlEngine} backed by DuckDB-WASM.
84
+ *
85
+ * @param options - The bundle URLs, or a pre-built instance.
86
+ * @returns The engine seam, ready to hand to `createConnectorsPanel({ transport: { sql } })`.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const sql = await createDuckDbEngine({
91
+ * bundle: { mainModule: wasmUrl, mainWorker: workerUrl },
92
+ * })
93
+ * const panel = createConnectorsPanel({ transport: { fetch: browserFetch(), sql } })
94
+ * ```
95
+ */
96
+ declare function createDuckDbEngine(options: CreateDuckDbEngineOptions): Promise<XenoSqlEngine>;
97
+ /**
98
+ * Convert one Arrow row proxy into a plain object.
99
+ *
100
+ * @param row - A `StructRowProxy` from `Table.toArray()`.
101
+ * @returns A plain, serializable object.
102
+ */
103
+ declare function unwrapArrowRow(row: unknown): Record<string, unknown>;
104
+ /**
105
+ * Recursively convert an Arrow value into something plain and serializable.
106
+ *
107
+ * TRAP 3 and the tail of TRAP 4 both live here. `toJSON()` on a row is **shallow**: a nested struct
108
+ * comes back as another proxy and a list column as an Arrow `Vector`, neither of which a panel can
109
+ * render or a `.xapp` can hold. And a `bigint` that survived the config cast — DuckDB's HUGEINT, or
110
+ * any build where the cast flag was ignored — would make `JSON.stringify` throw outright.
111
+ *
112
+ * @param value - Any value out of an Arrow row.
113
+ * @returns A plain value.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * unwrapArrowValue(123n) // → 123
118
+ * unwrapArrowValue(9007199254740993n) // → '9007199254740993' (precision kept, as text)
119
+ * ```
120
+ */
121
+ declare function unwrapArrowValue(value: unknown): unknown;
122
+
123
+ export { type CreateDuckDbEngineOptions, type DuckDbBundle, createDuckDbEngine, unwrapArrowRow, unwrapArrowValue };
@@ -0,0 +1,142 @@
1
+ import {
2
+ isSafeTableName
3
+ } from "../chunk-DJJGZ6U4.js";
4
+ import "../chunk-2KG3PWR4.js";
5
+
6
+ // src/data/connectors/duckdb.ts
7
+ async function createDuckDbEngine(options) {
8
+ const duckdb = options.module ?? await importDuckDb();
9
+ let db = options.db;
10
+ let worker = null;
11
+ if (!db) {
12
+ worker = new Worker(options.bundle.mainWorker);
13
+ const Logger = options.debug ? duckdb.ConsoleLogger : duckdb.VoidLogger ?? duckdb.ConsoleLogger;
14
+ db = new duckdb.AsyncDuckDB(new Logger(), worker);
15
+ await db.instantiate(options.bundle.mainModule, options.bundle.pthreadWorker ?? null);
16
+ }
17
+ await db.open({
18
+ query: {
19
+ // TRAP 4: without these, `count(*)` returns a `bigint` that `JSON.stringify` refuses, and a
20
+ // DECIMAL returns a raw Uint32Array of 128-bit limbs.
21
+ castBigIntToDouble: true,
22
+ castDecimalToDouble: true
23
+ }
24
+ });
25
+ const connection = await db.connect();
26
+ await lockDown(connection);
27
+ const registered = /* @__PURE__ */ new Set();
28
+ return {
29
+ async register(tables) {
30
+ for (const table of tables) {
31
+ if (!isSafeTableName(table.name)) {
32
+ throw new Error(`"${table.name}" is not a usable table name.`);
33
+ }
34
+ const fileName = `${table.name}.json`;
35
+ await db.registerFileText(fileName, JSON.stringify(table.rows ?? []));
36
+ try {
37
+ await connection.insertJSONFromPath(fileName, {
38
+ schema: "main",
39
+ name: table.name,
40
+ // ROW_ARRAY: we know the shape, so inference is skipped.
41
+ shape: "row-array",
42
+ // Replace rather than append: a refresh must not double the rows.
43
+ create: true
44
+ });
45
+ registered.add(table.name);
46
+ } finally {
47
+ await db.dropFile(fileName).catch(() => {
48
+ });
49
+ }
50
+ }
51
+ },
52
+ async query(sql) {
53
+ const table = await connection.query(sql);
54
+ return table.toArray().map((row) => unwrapArrowRow(row));
55
+ },
56
+ async dispose() {
57
+ for (const name of registered) {
58
+ await connection.query(`DROP TABLE IF EXISTS ${JSON.stringify(name)}`).catch(() => {
59
+ });
60
+ }
61
+ registered.clear();
62
+ await connection.close().catch(() => {
63
+ });
64
+ if (!options.db) await db?.terminate().catch(() => {
65
+ });
66
+ worker?.terminate();
67
+ }
68
+ };
69
+ }
70
+ async function importDuckDb() {
71
+ try {
72
+ const specifier = "@duckdb/duckdb-wasm";
73
+ return await import(
74
+ /* @vite-ignore */
75
+ /* webpackIgnore: true */
76
+ specifier
77
+ );
78
+ } catch (error) {
79
+ throw new Error(
80
+ "In-memory SQL needs @duckdb/duckdb-wasm, which is not installed. Install it, or supply a SQL engine through the transport seam. " + (error instanceof Error ? error.message : "")
81
+ );
82
+ }
83
+ }
84
+ async function lockDown(connection) {
85
+ const statements = [
86
+ "SET enable_external_access = false",
87
+ "SET autoinstall_known_extensions = false",
88
+ "SET autoload_known_extensions = false",
89
+ "SET allow_community_extensions = false",
90
+ // LAST. Anything after this cannot be changed.
91
+ "SET lock_configuration = true"
92
+ ];
93
+ for (const statement of statements) {
94
+ await connection.query(statement).catch(() => {
95
+ });
96
+ }
97
+ }
98
+ function unwrapArrowRow(row) {
99
+ if (row === null || row === void 0) return {};
100
+ const shallow = typeof row.toJSON === "function" ? row.toJSON() : row;
101
+ const out = {};
102
+ for (const [key, value] of Object.entries(shallow)) out[key] = unwrapArrowValue(value);
103
+ return out;
104
+ }
105
+ function unwrapArrowValue(value) {
106
+ if (value === null || value === void 0) return null;
107
+ if (typeof value === "bigint") {
108
+ return value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value.toString();
109
+ }
110
+ if (typeof value !== "object") return value;
111
+ if (value instanceof Date) return value.toISOString();
112
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
113
+ return Array.from(value, (item) => unwrapArrowValue(item));
114
+ }
115
+ if (Array.isArray(value)) return value.map((item) => unwrapArrowValue(item));
116
+ const vector = value;
117
+ if (typeof vector.toArray === "function") {
118
+ return vector.toArray().map((item) => unwrapArrowValue(item));
119
+ }
120
+ const nested = value;
121
+ if (typeof nested.toJSON === "function") {
122
+ const plain = nested.toJSON();
123
+ if (plain !== null && typeof plain === "object" && !Array.isArray(plain)) {
124
+ const out2 = {};
125
+ for (const [key, item] of Object.entries(plain)) {
126
+ out2[key] = unwrapArrowValue(item);
127
+ }
128
+ return out2;
129
+ }
130
+ return plain;
131
+ }
132
+ const out = {};
133
+ for (const [key, item] of Object.entries(value)) {
134
+ out[key] = unwrapArrowValue(item);
135
+ }
136
+ return out;
137
+ }
138
+ export {
139
+ createDuckDbEngine,
140
+ unwrapArrowRow,
141
+ unwrapArrowValue
142
+ };