@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.
package/dist/index.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // package.json
4
4
  var name = "@onyx.dev/onyx-database";
5
- var version = "2.3.0";
5
+ var version = "2.4.1";
6
6
 
7
7
  // src/config/defaults.ts
8
8
  var DEFAULT_BASE_URL = "https://api.onyx.dev";
@@ -1194,6 +1194,374 @@ function computeSchemaDiff(apiSchema, localSchema) {
1194
1194
  return { newTables, removedTables, changedTables };
1195
1195
  }
1196
1196
 
1197
+ // src/helpers/query-formatters.ts
1198
+ var DEFAULT_TABLE_OPTIONS = {
1199
+ headers: true,
1200
+ maxColumnWidth: 80,
1201
+ flattenNestedObjects: false,
1202
+ nestedSeparator: ".",
1203
+ nullValue: ""
1204
+ };
1205
+ var DEFAULT_TREE_OPTIONS = {
1206
+ rootLabel: "results",
1207
+ keyField: "",
1208
+ includeRoot: true,
1209
+ maxDepth: Number.POSITIVE_INFINITY,
1210
+ nullValue: ""
1211
+ };
1212
+ var DEFAULT_CSV_OPTIONS = {
1213
+ headers: true,
1214
+ delimiter: ",",
1215
+ quote: '"',
1216
+ escape: '"',
1217
+ newline: "\n",
1218
+ flattenNestedObjects: true,
1219
+ nestedSeparator: ".",
1220
+ nullValue: ""
1221
+ };
1222
+ var DEFAULT_JSON_OPTIONS = {
1223
+ pretty: true,
1224
+ indent: 2
1225
+ };
1226
+ function isDate(value) {
1227
+ return value instanceof Date;
1228
+ }
1229
+ function isRecord(value) {
1230
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !isDate(value);
1231
+ }
1232
+ function normalizeRecord(value) {
1233
+ if (isRecord(value)) {
1234
+ return value;
1235
+ }
1236
+ return { value };
1237
+ }
1238
+ function normalizeScalar(value, nullValue) {
1239
+ if (value == null) return nullValue;
1240
+ if (isDate(value)) return value.toISOString();
1241
+ if (typeof value === "string") return value;
1242
+ if (typeof value === "bigint") return value.toString();
1243
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1244
+ return JSON.stringify(value);
1245
+ }
1246
+ function inlineValue(value, nullValue) {
1247
+ if (value == null) return nullValue;
1248
+ if (isDate(value)) return value.toISOString();
1249
+ if (Array.isArray(value)) {
1250
+ return value.length === 0 ? "[]" : `[${value.map((entry) => inlineValue(entry, nullValue)).join(", ")}]`;
1251
+ }
1252
+ if (isRecord(value)) {
1253
+ const entries = Object.entries(value);
1254
+ if (entries.length === 0) return "{}";
1255
+ return entries.map(([key, entry]) => `${key}=${inlineValue(entry, nullValue)}`).join(", ");
1256
+ }
1257
+ if (typeof value === "bigint") return value.toString();
1258
+ return String(value);
1259
+ }
1260
+ function escapeTableCell(value) {
1261
+ return value.replace(/\r/g, "\\r").replace(/\n/g, "\\n");
1262
+ }
1263
+ function truncate(value, maxWidth) {
1264
+ if (maxWidth < 1) return "";
1265
+ if (value.length <= maxWidth) return value;
1266
+ if (maxWidth <= 3) return ".".repeat(maxWidth);
1267
+ return `${value.slice(0, Math.max(0, maxWidth - 3))}...`;
1268
+ }
1269
+ function padRight(value, width) {
1270
+ if (value.length >= width) return value;
1271
+ return value + " ".repeat(width - value.length);
1272
+ }
1273
+ function flattenValue(value, separator, prefix, out) {
1274
+ if (isRecord(value)) {
1275
+ const entries = Object.entries(value);
1276
+ if (entries.length === 0) {
1277
+ out[prefix] = {};
1278
+ return;
1279
+ }
1280
+ for (const [key, entry] of entries) {
1281
+ const path = prefix ? `${prefix}${separator}${key}` : key;
1282
+ flattenValue(entry, separator, path, out);
1283
+ }
1284
+ return;
1285
+ }
1286
+ if (Array.isArray(value)) {
1287
+ if (value.length === 0) {
1288
+ out[prefix] = [];
1289
+ return;
1290
+ }
1291
+ value.forEach((entry, index) => {
1292
+ const path = prefix ? `${prefix}${separator}${index}` : String(index);
1293
+ flattenValue(entry, separator, path, out);
1294
+ });
1295
+ return;
1296
+ }
1297
+ out[prefix] = value;
1298
+ }
1299
+ function toFlatRow(record, separator) {
1300
+ const out = {};
1301
+ for (const [key, value] of Object.entries(record)) {
1302
+ flattenValue(value, separator, key, out);
1303
+ }
1304
+ return out;
1305
+ }
1306
+ function discoveredColumns(rows) {
1307
+ const columns = [];
1308
+ const seen = /* @__PURE__ */ new Set();
1309
+ for (const row of rows) {
1310
+ for (const key of Object.keys(row)) {
1311
+ if (!seen.has(key)) {
1312
+ seen.add(key);
1313
+ columns.push(key);
1314
+ }
1315
+ }
1316
+ }
1317
+ return columns;
1318
+ }
1319
+ function orderColumns(discovered, preferred, flattenNestedObjects, separator) {
1320
+ if (!preferred || preferred.length === 0) return discovered;
1321
+ const ordered = [];
1322
+ const seen = /* @__PURE__ */ new Set();
1323
+ const addColumn = (column) => {
1324
+ if (!seen.has(column)) {
1325
+ seen.add(column);
1326
+ ordered.push(column);
1327
+ }
1328
+ };
1329
+ for (const field of preferred) {
1330
+ if (flattenNestedObjects) {
1331
+ let matched = false;
1332
+ for (const column of discovered) {
1333
+ if (column === field || column.startsWith(`${field}${separator}`)) {
1334
+ addColumn(column);
1335
+ matched = true;
1336
+ }
1337
+ }
1338
+ if (!matched) {
1339
+ addColumn(field);
1340
+ }
1341
+ continue;
1342
+ }
1343
+ addColumn(field);
1344
+ }
1345
+ for (const column of discovered) {
1346
+ addColumn(column);
1347
+ }
1348
+ return ordered;
1349
+ }
1350
+ function normalizeTableRows(records, preferredColumns, options) {
1351
+ const normalized = records.map(normalizeRecord);
1352
+ if (options.flattenNestedObjects) {
1353
+ const flatRows = normalized.map((record) => toFlatRow(record, options.nestedSeparator));
1354
+ const columns2 = orderColumns(
1355
+ discoveredColumns(flatRows),
1356
+ preferredColumns,
1357
+ true,
1358
+ options.nestedSeparator
1359
+ );
1360
+ const rows2 = flatRows.map((row) => columns2.map((column) => escapeTableCell(truncate(
1361
+ inlineValue(row[column], options.nullValue),
1362
+ options.maxColumnWidth
1363
+ ))));
1364
+ return { columns: columns2, rows: rows2 };
1365
+ }
1366
+ const topLevelRows = normalized.map((record) => ({ ...record }));
1367
+ const columns = orderColumns(
1368
+ discoveredColumns(topLevelRows),
1369
+ preferredColumns,
1370
+ false,
1371
+ options.nestedSeparator
1372
+ );
1373
+ const rows = topLevelRows.map((row) => columns.map((column) => escapeTableCell(truncate(
1374
+ inlineValue(row[column], options.nullValue),
1375
+ options.maxColumnWidth
1376
+ ))));
1377
+ return { columns, rows };
1378
+ }
1379
+ function formatQueryResultsAsTable(records, options, preferredColumns) {
1380
+ const final = { ...DEFAULT_TABLE_OPTIONS, ...options };
1381
+ const { columns, rows } = normalizeTableRows(records, preferredColumns, final);
1382
+ if (columns.length === 0) return "";
1383
+ const headerCells = columns.map((column) => truncate(column, final.maxColumnWidth));
1384
+ const widths = columns.map((_, index) => {
1385
+ const headerWidth = final.headers ? headerCells[index].length : 0;
1386
+ const rowWidth = rows.reduce((max2, row) => Math.max(max2, row[index].length), 0);
1387
+ return Math.max(headerWidth, rowWidth);
1388
+ });
1389
+ const border = (left, join, right) => `${left}${widths.map((width) => "\u2500".repeat(width + 2)).join(join)}${right}`;
1390
+ const renderRow = (cells) => `\u2502 ${cells.map((cell, index) => padRight(cell, widths[index])).join(" \u2502 ")} \u2502`;
1391
+ const lines = [border("\u250C", "\u252C", "\u2510")];
1392
+ if (final.headers) {
1393
+ lines.push(renderRow(headerCells));
1394
+ lines.push(border("\u251C", "\u253C", "\u2524"));
1395
+ }
1396
+ for (const row of rows) {
1397
+ lines.push(renderRow(row));
1398
+ }
1399
+ lines.push(border("\u2514", "\u2534", "\u2518"));
1400
+ return lines.join("\n");
1401
+ }
1402
+ function encodeCsvCell(value, options) {
1403
+ const escapedQuote = `${options.escape}${options.quote}`;
1404
+ const escaped = value.split(options.quote).join(escapedQuote);
1405
+ const needsQuoting = escaped.includes(options.delimiter) || escaped.includes(options.quote) || escaped.includes("\n") || escaped.includes("\r");
1406
+ if (!needsQuoting) return escaped;
1407
+ return `${options.quote}${escaped}${options.quote}`;
1408
+ }
1409
+ function normalizeCsvRows(records, preferredColumns, options) {
1410
+ const normalized = records.map(normalizeRecord);
1411
+ if (options.flattenNestedObjects) {
1412
+ const flatRows = normalized.map((record) => toFlatRow(record, options.nestedSeparator));
1413
+ const columns2 = orderColumns(
1414
+ discoveredColumns(flatRows),
1415
+ preferredColumns,
1416
+ true,
1417
+ options.nestedSeparator
1418
+ );
1419
+ const rows2 = flatRows.map((row) => columns2.map((column) => normalizeScalar(row[column], options.nullValue)));
1420
+ return { columns: columns2, rows: rows2 };
1421
+ }
1422
+ const topLevelRows = normalized.map((record) => ({ ...record }));
1423
+ const columns = orderColumns(
1424
+ discoveredColumns(topLevelRows),
1425
+ preferredColumns,
1426
+ false,
1427
+ options.nestedSeparator
1428
+ );
1429
+ const rows = topLevelRows.map((row) => columns.map((column) => inlineValue(row[column], options.nullValue)));
1430
+ return { columns, rows };
1431
+ }
1432
+ function formatQueryResultsAsCsv(records, options, preferredColumns) {
1433
+ const final = { ...DEFAULT_CSV_OPTIONS, ...options };
1434
+ const { columns, rows } = normalizeCsvRows(records, preferredColumns, final);
1435
+ const lines = [];
1436
+ if (final.headers && columns.length > 0) {
1437
+ lines.push(columns.map((column) => encodeCsvCell(column, final)).join(final.delimiter));
1438
+ }
1439
+ for (const row of rows) {
1440
+ lines.push(row.map((cell) => encodeCsvCell(cell, final)).join(final.delimiter));
1441
+ }
1442
+ return lines.join(final.newline);
1443
+ }
1444
+ function normalizeJsonValue(value) {
1445
+ if (value === void 0) return null;
1446
+ if (value === null) return null;
1447
+ if (isDate(value)) return value.toISOString();
1448
+ if (typeof value === "bigint") return value.toString();
1449
+ if (Array.isArray(value)) return value.map((entry) => normalizeJsonValue(entry));
1450
+ if (isRecord(value)) {
1451
+ const out = {};
1452
+ for (const [key, entry] of Object.entries(value)) {
1453
+ out[key] = normalizeJsonValue(entry);
1454
+ }
1455
+ return out;
1456
+ }
1457
+ return value;
1458
+ }
1459
+ function formatQueryResultsAsJson(records, options) {
1460
+ const final = { ...DEFAULT_JSON_OPTIONS, ...options };
1461
+ const normalized = records.map((record) => normalizeJsonValue(normalizeRecord(record)));
1462
+ return final.pretty ? JSON.stringify(normalized, null, final.indent) : JSON.stringify(normalized);
1463
+ }
1464
+ function findDefaultTreeKeyField(record) {
1465
+ for (const candidate of ["code", "id", "name"]) {
1466
+ if (candidate in record) return candidate;
1467
+ }
1468
+ const keys = Object.keys(record);
1469
+ return keys.length > 0 ? keys[0] : void 0;
1470
+ }
1471
+ function buildTreeNodesFromValue(value, options, depth) {
1472
+ if (depth >= options.maxDepth) {
1473
+ return [{ label: inlineValue(value, options.nullValue) }];
1474
+ }
1475
+ if (Array.isArray(value)) {
1476
+ return value.map((entry, index) => {
1477
+ if (!isRecord(entry) && !Array.isArray(entry)) {
1478
+ return { label: `[${index}]: ${inlineValue(entry, options.nullValue)}` };
1479
+ }
1480
+ return {
1481
+ label: `[${index}]`,
1482
+ children: buildTreeNodesFromValue(entry, options, depth + 1)
1483
+ };
1484
+ });
1485
+ }
1486
+ return Object.entries(value).map(([key, entry]) => {
1487
+ if (isRecord(entry) || Array.isArray(entry)) {
1488
+ if (depth + 1 >= options.maxDepth) {
1489
+ return { label: `${key}: ${inlineValue(entry, options.nullValue)}` };
1490
+ }
1491
+ return {
1492
+ label: key,
1493
+ children: buildTreeNodesFromValue(entry, options, depth + 1)
1494
+ };
1495
+ }
1496
+ return { label: `${key}: ${inlineValue(entry, options.nullValue)}` };
1497
+ });
1498
+ }
1499
+ function buildRecordTreeNode(record, index, options) {
1500
+ const keyField = options.keyField || findDefaultTreeKeyField(record);
1501
+ const entries = Object.entries(record);
1502
+ if (!keyField || !(keyField in record)) {
1503
+ return {
1504
+ label: `row ${index + 1}`,
1505
+ children: buildTreeNodesFromValue(record, options, 0)
1506
+ };
1507
+ }
1508
+ const labelValue = inlineValue(record[keyField], options.nullValue);
1509
+ const childEntries = entries.filter(([key]) => key !== keyField);
1510
+ return {
1511
+ label: labelValue,
1512
+ children: childEntries.length > 0 ? childEntries.map(([key, value]) => {
1513
+ if (isRecord(value) || Array.isArray(value)) {
1514
+ if (1 > options.maxDepth) {
1515
+ return { label: `${key}: ${inlineValue(value, options.nullValue)}` };
1516
+ }
1517
+ return {
1518
+ label: key,
1519
+ children: buildTreeNodesFromValue(value, options, 1)
1520
+ };
1521
+ }
1522
+ return { label: `${key}: ${inlineValue(value, options.nullValue)}` };
1523
+ }) : void 0
1524
+ };
1525
+ }
1526
+ function renderTreeNodes(nodes, prefix = "") {
1527
+ const lines = [];
1528
+ nodes.forEach((node, index) => {
1529
+ const isLast = index === nodes.length - 1;
1530
+ const branch = isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
1531
+ lines.push(`${prefix}${branch}${node.label}`);
1532
+ if (node.children && node.children.length > 0) {
1533
+ const childPrefix = `${prefix}${isLast ? " " : "\u2502 "}`;
1534
+ lines.push(...renderTreeNodes(node.children, childPrefix));
1535
+ }
1536
+ });
1537
+ return lines;
1538
+ }
1539
+ function formatQueryResultsAsTree(records, options) {
1540
+ const final = { ...DEFAULT_TREE_OPTIONS, ...options };
1541
+ const nodes = records.map((record, index) => buildRecordTreeNode(normalizeRecord(record), index, final));
1542
+ if (!final.includeRoot) {
1543
+ return renderTreeNodes(nodes).join("\n");
1544
+ }
1545
+ const lines = [final.rootLabel];
1546
+ if (nodes.length > 0) {
1547
+ lines.push(...renderTreeNodes(nodes));
1548
+ }
1549
+ return lines.join("\n");
1550
+ }
1551
+ async function collectAllQueryRecords(getPage, initialNextPage) {
1552
+ const records = [];
1553
+ let nextPage = initialNextPage;
1554
+ while (true) {
1555
+ const page = await getPage(nextPage);
1556
+ if (Array.isArray(page.records)) {
1557
+ records.push(...page.records);
1558
+ }
1559
+ if (!page.nextPage) break;
1560
+ nextPage = page.nextPage;
1561
+ }
1562
+ return records;
1563
+ }
1564
+
1197
1565
  // src/impl/onyx-core.ts
1198
1566
  var DEFAULT_CACHE_TTL = 5 * 60 * 1e3;
1199
1567
  function toSingleCondition(criteria) {
@@ -1722,7 +2090,7 @@ var OnyxDatabaseImpl = class {
1722
2090
  };
1723
2091
  var QueryBuilderImpl = class {
1724
2092
  db;
1725
- table;
2093
+ tableName;
1726
2094
  fields = null;
1727
2095
  resolvers = null;
1728
2096
  conditions = null;
@@ -1741,12 +2109,12 @@ var QueryBuilderImpl = class {
1741
2109
  onItemListener = null;
1742
2110
  constructor(db, table, partition) {
1743
2111
  this.db = db;
1744
- this.table = table;
2112
+ this.tableName = table;
1745
2113
  this.partitionValue = partition;
1746
2114
  }
1747
2115
  ensureTable() {
1748
- if (!this.table) throw new Error("Table is not defined. Call from(<table>) first.");
1749
- return this.table;
2116
+ if (!this.tableName) throw new Error("Table is not defined. Call from(<table>) first.");
2117
+ return this.tableName;
1750
2118
  }
1751
2119
  serializableConditions() {
1752
2120
  return normalizeCondition(this.conditions);
@@ -1754,7 +2122,7 @@ var QueryBuilderImpl = class {
1754
2122
  toSelectQuery() {
1755
2123
  return {
1756
2124
  type: "SelectQuery",
1757
- table: this.table,
2125
+ table: this.tableName,
1758
2126
  fields: this.fields,
1759
2127
  conditions: this.serializableConditions(),
1760
2128
  sort: this.sort,
@@ -1775,13 +2143,25 @@ var QueryBuilderImpl = class {
1775
2143
  partition: this.partitionValue ?? null
1776
2144
  };
1777
2145
  }
2146
+ async getAllRecordsForFormatting() {
2147
+ if (this.mode !== "select") throw new Error("Formatting is only applicable in select mode.");
2148
+ const table = this.ensureTable();
2149
+ const select = this.toSelectQuery();
2150
+ const pageSize = this.pageSizeValue ?? void 0;
2151
+ const initialNextPage = this.nextPageValue ?? void 0;
2152
+ return collectAllQueryRecords((nextPage) => this.db._queryPage(table, select, {
2153
+ pageSize,
2154
+ nextPage,
2155
+ partition: this.partitionValue
2156
+ }), initialNextPage);
2157
+ }
1778
2158
  toSerializableQueryObject() {
1779
2159
  const table = this.ensureTable();
1780
2160
  const payload = this.mode === "update" ? this.toUpdateQuery() : this.toSelectQuery();
1781
2161
  return { ...payload, table };
1782
2162
  }
1783
2163
  from(table) {
1784
- this.table = table;
2164
+ this.tableName = table;
1785
2165
  return this;
1786
2166
  }
1787
2167
  select(...fields) {
@@ -1913,6 +2293,26 @@ var QueryBuilderImpl = class {
1913
2293
  async one() {
1914
2294
  return this.firstOrNull();
1915
2295
  }
2296
+ async table(options) {
2297
+ return formatQueryResultsAsTable(
2298
+ await this.getAllRecordsForFormatting(),
2299
+ options,
2300
+ this.fields ?? void 0
2301
+ );
2302
+ }
2303
+ async tree(options) {
2304
+ return formatQueryResultsAsTree(await this.getAllRecordsForFormatting(), options);
2305
+ }
2306
+ async csv(options) {
2307
+ return formatQueryResultsAsCsv(
2308
+ await this.getAllRecordsForFormatting(),
2309
+ options,
2310
+ this.fields ?? void 0
2311
+ );
2312
+ }
2313
+ async json(options) {
2314
+ return formatQueryResultsAsJson(await this.getAllRecordsForFormatting(), options);
2315
+ }
1916
2316
  async delete() {
1917
2317
  if (this.mode !== "select") throw new Error("delete() is only applicable in select mode.");
1918
2318
  const table = this.ensureTable();