@onyx.dev/onyx-database 2.3.0 → 2.4.1

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.
@@ -1325,6 +1325,374 @@ ${bodyLines.join("\n")}
1325
1325
  `;
1326
1326
  }
1327
1327
 
1328
+ // src/helpers/query-formatters.ts
1329
+ var DEFAULT_TABLE_OPTIONS = {
1330
+ headers: true,
1331
+ maxColumnWidth: 80,
1332
+ flattenNestedObjects: false,
1333
+ nestedSeparator: ".",
1334
+ nullValue: ""
1335
+ };
1336
+ var DEFAULT_TREE_OPTIONS = {
1337
+ rootLabel: "results",
1338
+ keyField: "",
1339
+ includeRoot: true,
1340
+ maxDepth: Number.POSITIVE_INFINITY,
1341
+ nullValue: ""
1342
+ };
1343
+ var DEFAULT_CSV_OPTIONS = {
1344
+ headers: true,
1345
+ delimiter: ",",
1346
+ quote: '"',
1347
+ escape: '"',
1348
+ newline: "\n",
1349
+ flattenNestedObjects: true,
1350
+ nestedSeparator: ".",
1351
+ nullValue: ""
1352
+ };
1353
+ var DEFAULT_JSON_OPTIONS = {
1354
+ pretty: true,
1355
+ indent: 2
1356
+ };
1357
+ function isDate(value) {
1358
+ return value instanceof Date;
1359
+ }
1360
+ function isRecord2(value) {
1361
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !isDate(value);
1362
+ }
1363
+ function normalizeRecord(value) {
1364
+ if (isRecord2(value)) {
1365
+ return value;
1366
+ }
1367
+ return { value };
1368
+ }
1369
+ function normalizeScalar(value, nullValue) {
1370
+ if (value == null) return nullValue;
1371
+ if (isDate(value)) return value.toISOString();
1372
+ if (typeof value === "string") return value;
1373
+ if (typeof value === "bigint") return value.toString();
1374
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1375
+ return JSON.stringify(value);
1376
+ }
1377
+ function inlineValue(value, nullValue) {
1378
+ if (value == null) return nullValue;
1379
+ if (isDate(value)) return value.toISOString();
1380
+ if (Array.isArray(value)) {
1381
+ return value.length === 0 ? "[]" : `[${value.map((entry) => inlineValue(entry, nullValue)).join(", ")}]`;
1382
+ }
1383
+ if (isRecord2(value)) {
1384
+ const entries = Object.entries(value);
1385
+ if (entries.length === 0) return "{}";
1386
+ return entries.map(([key, entry]) => `${key}=${inlineValue(entry, nullValue)}`).join(", ");
1387
+ }
1388
+ if (typeof value === "bigint") return value.toString();
1389
+ return String(value);
1390
+ }
1391
+ function escapeTableCell(value) {
1392
+ return value.replace(/\r/g, "\\r").replace(/\n/g, "\\n");
1393
+ }
1394
+ function truncate(value, maxWidth) {
1395
+ if (maxWidth < 1) return "";
1396
+ if (value.length <= maxWidth) return value;
1397
+ if (maxWidth <= 3) return ".".repeat(maxWidth);
1398
+ return `${value.slice(0, Math.max(0, maxWidth - 3))}...`;
1399
+ }
1400
+ function padRight(value, width) {
1401
+ if (value.length >= width) return value;
1402
+ return value + " ".repeat(width - value.length);
1403
+ }
1404
+ function flattenValue(value, separator, prefix, out) {
1405
+ if (isRecord2(value)) {
1406
+ const entries = Object.entries(value);
1407
+ if (entries.length === 0) {
1408
+ out[prefix] = {};
1409
+ return;
1410
+ }
1411
+ for (const [key, entry] of entries) {
1412
+ const path2 = prefix ? `${prefix}${separator}${key}` : key;
1413
+ flattenValue(entry, separator, path2, out);
1414
+ }
1415
+ return;
1416
+ }
1417
+ if (Array.isArray(value)) {
1418
+ if (value.length === 0) {
1419
+ out[prefix] = [];
1420
+ return;
1421
+ }
1422
+ value.forEach((entry, index) => {
1423
+ const path2 = prefix ? `${prefix}${separator}${index}` : String(index);
1424
+ flattenValue(entry, separator, path2, out);
1425
+ });
1426
+ return;
1427
+ }
1428
+ out[prefix] = value;
1429
+ }
1430
+ function toFlatRow(record, separator) {
1431
+ const out = {};
1432
+ for (const [key, value] of Object.entries(record)) {
1433
+ flattenValue(value, separator, key, out);
1434
+ }
1435
+ return out;
1436
+ }
1437
+ function discoveredColumns(rows) {
1438
+ const columns = [];
1439
+ const seen = /* @__PURE__ */ new Set();
1440
+ for (const row of rows) {
1441
+ for (const key of Object.keys(row)) {
1442
+ if (!seen.has(key)) {
1443
+ seen.add(key);
1444
+ columns.push(key);
1445
+ }
1446
+ }
1447
+ }
1448
+ return columns;
1449
+ }
1450
+ function orderColumns(discovered, preferred, flattenNestedObjects, separator) {
1451
+ if (!preferred || preferred.length === 0) return discovered;
1452
+ const ordered = [];
1453
+ const seen = /* @__PURE__ */ new Set();
1454
+ const addColumn = (column) => {
1455
+ if (!seen.has(column)) {
1456
+ seen.add(column);
1457
+ ordered.push(column);
1458
+ }
1459
+ };
1460
+ for (const field of preferred) {
1461
+ if (flattenNestedObjects) {
1462
+ let matched = false;
1463
+ for (const column of discovered) {
1464
+ if (column === field || column.startsWith(`${field}${separator}`)) {
1465
+ addColumn(column);
1466
+ matched = true;
1467
+ }
1468
+ }
1469
+ if (!matched) {
1470
+ addColumn(field);
1471
+ }
1472
+ continue;
1473
+ }
1474
+ addColumn(field);
1475
+ }
1476
+ for (const column of discovered) {
1477
+ addColumn(column);
1478
+ }
1479
+ return ordered;
1480
+ }
1481
+ function normalizeTableRows(records, preferredColumns, options) {
1482
+ const normalized = records.map(normalizeRecord);
1483
+ if (options.flattenNestedObjects) {
1484
+ const flatRows = normalized.map((record) => toFlatRow(record, options.nestedSeparator));
1485
+ const columns2 = orderColumns(
1486
+ discoveredColumns(flatRows),
1487
+ preferredColumns,
1488
+ true,
1489
+ options.nestedSeparator
1490
+ );
1491
+ const rows2 = flatRows.map((row) => columns2.map((column) => escapeTableCell(truncate(
1492
+ inlineValue(row[column], options.nullValue),
1493
+ options.maxColumnWidth
1494
+ ))));
1495
+ return { columns: columns2, rows: rows2 };
1496
+ }
1497
+ const topLevelRows = normalized.map((record) => ({ ...record }));
1498
+ const columns = orderColumns(
1499
+ discoveredColumns(topLevelRows),
1500
+ preferredColumns,
1501
+ false,
1502
+ options.nestedSeparator
1503
+ );
1504
+ const rows = topLevelRows.map((row) => columns.map((column) => escapeTableCell(truncate(
1505
+ inlineValue(row[column], options.nullValue),
1506
+ options.maxColumnWidth
1507
+ ))));
1508
+ return { columns, rows };
1509
+ }
1510
+ function formatQueryResultsAsTable(records, options, preferredColumns) {
1511
+ const final = { ...DEFAULT_TABLE_OPTIONS, ...options };
1512
+ const { columns, rows } = normalizeTableRows(records, preferredColumns, final);
1513
+ if (columns.length === 0) return "";
1514
+ const headerCells = columns.map((column) => truncate(column, final.maxColumnWidth));
1515
+ const widths = columns.map((_, index) => {
1516
+ const headerWidth = final.headers ? headerCells[index].length : 0;
1517
+ const rowWidth = rows.reduce((max, row) => Math.max(max, row[index].length), 0);
1518
+ return Math.max(headerWidth, rowWidth);
1519
+ });
1520
+ const border = (left, join, right) => `${left}${widths.map((width) => "\u2500".repeat(width + 2)).join(join)}${right}`;
1521
+ const renderRow = (cells) => `\u2502 ${cells.map((cell, index) => padRight(cell, widths[index])).join(" \u2502 ")} \u2502`;
1522
+ const lines = [border("\u250C", "\u252C", "\u2510")];
1523
+ if (final.headers) {
1524
+ lines.push(renderRow(headerCells));
1525
+ lines.push(border("\u251C", "\u253C", "\u2524"));
1526
+ }
1527
+ for (const row of rows) {
1528
+ lines.push(renderRow(row));
1529
+ }
1530
+ lines.push(border("\u2514", "\u2534", "\u2518"));
1531
+ return lines.join("\n");
1532
+ }
1533
+ function encodeCsvCell(value, options) {
1534
+ const escapedQuote = `${options.escape}${options.quote}`;
1535
+ const escaped = value.split(options.quote).join(escapedQuote);
1536
+ const needsQuoting = escaped.includes(options.delimiter) || escaped.includes(options.quote) || escaped.includes("\n") || escaped.includes("\r");
1537
+ if (!needsQuoting) return escaped;
1538
+ return `${options.quote}${escaped}${options.quote}`;
1539
+ }
1540
+ function normalizeCsvRows(records, preferredColumns, options) {
1541
+ const normalized = records.map(normalizeRecord);
1542
+ if (options.flattenNestedObjects) {
1543
+ const flatRows = normalized.map((record) => toFlatRow(record, options.nestedSeparator));
1544
+ const columns2 = orderColumns(
1545
+ discoveredColumns(flatRows),
1546
+ preferredColumns,
1547
+ true,
1548
+ options.nestedSeparator
1549
+ );
1550
+ const rows2 = flatRows.map((row) => columns2.map((column) => normalizeScalar(row[column], options.nullValue)));
1551
+ return { columns: columns2, rows: rows2 };
1552
+ }
1553
+ const topLevelRows = normalized.map((record) => ({ ...record }));
1554
+ const columns = orderColumns(
1555
+ discoveredColumns(topLevelRows),
1556
+ preferredColumns,
1557
+ false,
1558
+ options.nestedSeparator
1559
+ );
1560
+ const rows = topLevelRows.map((row) => columns.map((column) => inlineValue(row[column], options.nullValue)));
1561
+ return { columns, rows };
1562
+ }
1563
+ function formatQueryResultsAsCsv(records, options, preferredColumns) {
1564
+ const final = { ...DEFAULT_CSV_OPTIONS, ...options };
1565
+ const { columns, rows } = normalizeCsvRows(records, preferredColumns, final);
1566
+ const lines = [];
1567
+ if (final.headers && columns.length > 0) {
1568
+ lines.push(columns.map((column) => encodeCsvCell(column, final)).join(final.delimiter));
1569
+ }
1570
+ for (const row of rows) {
1571
+ lines.push(row.map((cell) => encodeCsvCell(cell, final)).join(final.delimiter));
1572
+ }
1573
+ return lines.join(final.newline);
1574
+ }
1575
+ function normalizeJsonValue(value) {
1576
+ if (value === void 0) return null;
1577
+ if (value === null) return null;
1578
+ if (isDate(value)) return value.toISOString();
1579
+ if (typeof value === "bigint") return value.toString();
1580
+ if (Array.isArray(value)) return value.map((entry) => normalizeJsonValue(entry));
1581
+ if (isRecord2(value)) {
1582
+ const out = {};
1583
+ for (const [key, entry] of Object.entries(value)) {
1584
+ out[key] = normalizeJsonValue(entry);
1585
+ }
1586
+ return out;
1587
+ }
1588
+ return value;
1589
+ }
1590
+ function formatQueryResultsAsJson(records, options) {
1591
+ const final = { ...DEFAULT_JSON_OPTIONS, ...options };
1592
+ const normalized = records.map((record) => normalizeJsonValue(normalizeRecord(record)));
1593
+ return final.pretty ? JSON.stringify(normalized, null, final.indent) : JSON.stringify(normalized);
1594
+ }
1595
+ function findDefaultTreeKeyField(record) {
1596
+ for (const candidate of ["code", "id", "name"]) {
1597
+ if (candidate in record) return candidate;
1598
+ }
1599
+ const keys = Object.keys(record);
1600
+ return keys.length > 0 ? keys[0] : void 0;
1601
+ }
1602
+ function buildTreeNodesFromValue(value, options, depth) {
1603
+ if (depth >= options.maxDepth) {
1604
+ return [{ label: inlineValue(value, options.nullValue) }];
1605
+ }
1606
+ if (Array.isArray(value)) {
1607
+ return value.map((entry, index) => {
1608
+ if (!isRecord2(entry) && !Array.isArray(entry)) {
1609
+ return { label: `[${index}]: ${inlineValue(entry, options.nullValue)}` };
1610
+ }
1611
+ return {
1612
+ label: `[${index}]`,
1613
+ children: buildTreeNodesFromValue(entry, options, depth + 1)
1614
+ };
1615
+ });
1616
+ }
1617
+ return Object.entries(value).map(([key, entry]) => {
1618
+ if (isRecord2(entry) || Array.isArray(entry)) {
1619
+ if (depth + 1 >= options.maxDepth) {
1620
+ return { label: `${key}: ${inlineValue(entry, options.nullValue)}` };
1621
+ }
1622
+ return {
1623
+ label: key,
1624
+ children: buildTreeNodesFromValue(entry, options, depth + 1)
1625
+ };
1626
+ }
1627
+ return { label: `${key}: ${inlineValue(entry, options.nullValue)}` };
1628
+ });
1629
+ }
1630
+ function buildRecordTreeNode(record, index, options) {
1631
+ const keyField = options.keyField || findDefaultTreeKeyField(record);
1632
+ const entries = Object.entries(record);
1633
+ if (!keyField || !(keyField in record)) {
1634
+ return {
1635
+ label: `row ${index + 1}`,
1636
+ children: buildTreeNodesFromValue(record, options, 0)
1637
+ };
1638
+ }
1639
+ const labelValue = inlineValue(record[keyField], options.nullValue);
1640
+ const childEntries = entries.filter(([key]) => key !== keyField);
1641
+ return {
1642
+ label: labelValue,
1643
+ children: childEntries.length > 0 ? childEntries.map(([key, value]) => {
1644
+ if (isRecord2(value) || Array.isArray(value)) {
1645
+ if (1 > options.maxDepth) {
1646
+ return { label: `${key}: ${inlineValue(value, options.nullValue)}` };
1647
+ }
1648
+ return {
1649
+ label: key,
1650
+ children: buildTreeNodesFromValue(value, options, 1)
1651
+ };
1652
+ }
1653
+ return { label: `${key}: ${inlineValue(value, options.nullValue)}` };
1654
+ }) : void 0
1655
+ };
1656
+ }
1657
+ function renderTreeNodes(nodes, prefix = "") {
1658
+ const lines = [];
1659
+ nodes.forEach((node, index) => {
1660
+ const isLast = index === nodes.length - 1;
1661
+ const branch = isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
1662
+ lines.push(`${prefix}${branch}${node.label}`);
1663
+ if (node.children && node.children.length > 0) {
1664
+ const childPrefix = `${prefix}${isLast ? " " : "\u2502 "}`;
1665
+ lines.push(...renderTreeNodes(node.children, childPrefix));
1666
+ }
1667
+ });
1668
+ return lines;
1669
+ }
1670
+ function formatQueryResultsAsTree(records, options) {
1671
+ const final = { ...DEFAULT_TREE_OPTIONS, ...options };
1672
+ const nodes = records.map((record, index) => buildRecordTreeNode(normalizeRecord(record), index, final));
1673
+ if (!final.includeRoot) {
1674
+ return renderTreeNodes(nodes).join("\n");
1675
+ }
1676
+ const lines = [final.rootLabel];
1677
+ if (nodes.length > 0) {
1678
+ lines.push(...renderTreeNodes(nodes));
1679
+ }
1680
+ return lines.join("\n");
1681
+ }
1682
+ async function collectAllQueryRecords(getPage, initialNextPage) {
1683
+ const records = [];
1684
+ let nextPage = initialNextPage;
1685
+ while (true) {
1686
+ const page = await getPage(nextPage);
1687
+ if (Array.isArray(page.records)) {
1688
+ records.push(...page.records);
1689
+ }
1690
+ if (!page.nextPage) break;
1691
+ nextPage = page.nextPage;
1692
+ }
1693
+ return records;
1694
+ }
1695
+
1328
1696
  // src/impl/onyx-core.ts
1329
1697
  var DEFAULT_CACHE_TTL = 5 * 60 * 1e3;
1330
1698
  function toSingleCondition(criteria) {
@@ -1853,7 +2221,7 @@ var OnyxDatabaseImpl = class {
1853
2221
  };
1854
2222
  var QueryBuilderImpl = class {
1855
2223
  db;
1856
- table;
2224
+ tableName;
1857
2225
  fields = null;
1858
2226
  resolvers = null;
1859
2227
  conditions = null;
@@ -1872,12 +2240,12 @@ var QueryBuilderImpl = class {
1872
2240
  onItemListener = null;
1873
2241
  constructor(db, table, partition) {
1874
2242
  this.db = db;
1875
- this.table = table;
2243
+ this.tableName = table;
1876
2244
  this.partitionValue = partition;
1877
2245
  }
1878
2246
  ensureTable() {
1879
- if (!this.table) throw new Error("Table is not defined. Call from(<table>) first.");
1880
- return this.table;
2247
+ if (!this.tableName) throw new Error("Table is not defined. Call from(<table>) first.");
2248
+ return this.tableName;
1881
2249
  }
1882
2250
  serializableConditions() {
1883
2251
  return normalizeCondition(this.conditions);
@@ -1885,7 +2253,7 @@ var QueryBuilderImpl = class {
1885
2253
  toSelectQuery() {
1886
2254
  return {
1887
2255
  type: "SelectQuery",
1888
- table: this.table,
2256
+ table: this.tableName,
1889
2257
  fields: this.fields,
1890
2258
  conditions: this.serializableConditions(),
1891
2259
  sort: this.sort,
@@ -1906,13 +2274,25 @@ var QueryBuilderImpl = class {
1906
2274
  partition: this.partitionValue ?? null
1907
2275
  };
1908
2276
  }
2277
+ async getAllRecordsForFormatting() {
2278
+ if (this.mode !== "select") throw new Error("Formatting is only applicable in select mode.");
2279
+ const table = this.ensureTable();
2280
+ const select = this.toSelectQuery();
2281
+ const pageSize = this.pageSizeValue ?? void 0;
2282
+ const initialNextPage = this.nextPageValue ?? void 0;
2283
+ return collectAllQueryRecords((nextPage) => this.db._queryPage(table, select, {
2284
+ pageSize,
2285
+ nextPage,
2286
+ partition: this.partitionValue
2287
+ }), initialNextPage);
2288
+ }
1909
2289
  toSerializableQueryObject() {
1910
2290
  const table = this.ensureTable();
1911
2291
  const payload = this.mode === "update" ? this.toUpdateQuery() : this.toSelectQuery();
1912
2292
  return { ...payload, table };
1913
2293
  }
1914
2294
  from(table) {
1915
- this.table = table;
2295
+ this.tableName = table;
1916
2296
  return this;
1917
2297
  }
1918
2298
  select(...fields) {
@@ -2044,6 +2424,26 @@ var QueryBuilderImpl = class {
2044
2424
  async one() {
2045
2425
  return this.firstOrNull();
2046
2426
  }
2427
+ async table(options) {
2428
+ return formatQueryResultsAsTable(
2429
+ await this.getAllRecordsForFormatting(),
2430
+ options,
2431
+ this.fields ?? void 0
2432
+ );
2433
+ }
2434
+ async tree(options) {
2435
+ return formatQueryResultsAsTree(await this.getAllRecordsForFormatting(), options);
2436
+ }
2437
+ async csv(options) {
2438
+ return formatQueryResultsAsCsv(
2439
+ await this.getAllRecordsForFormatting(),
2440
+ options,
2441
+ this.fields ?? void 0
2442
+ );
2443
+ }
2444
+ async json(options) {
2445
+ return formatQueryResultsAsJson(await this.getAllRecordsForFormatting(), options);
2446
+ }
2047
2447
  async delete() {
2048
2448
  if (this.mode !== "select") throw new Error("delete() is only applicable in select mode.");
2049
2449
  const table = this.ensureTable();