@nmakarov/cli-toolkit 0.1.4 → 0.3.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 +113 -2
- package/dist/args.cjs +1 -1
- package/dist/args.cjs.map +1 -1
- package/dist/args.js +1 -1
- package/dist/args.js.map +1 -1
- package/dist/filedatabase.cjs +948 -0
- package/dist/filedatabase.cjs.map +1 -0
- package/dist/filedatabase.js +908 -0
- package/dist/filedatabase.js.map +1 -0
- package/dist/http-client.cjs +356 -0
- package/dist/http-client.cjs.map +1 -0
- package/dist/http-client.js +318 -0
- package/dist/http-client.js.map +1 -0
- package/dist/index.cjs +901 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +897 -7
- package/dist/index.js.map +1 -1
- package/dist/mock-server.cjs +1483 -0
- package/dist/mock-server.cjs.map +1 -0
- package/dist/mock-server.js +1445 -0
- package/dist/mock-server.js.map +1 -0
- package/dist/params.cjs +0 -2
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +0 -2
- package/dist/params.js.map +1 -1
- package/package.json +33 -3
package/dist/index.js
CHANGED
|
@@ -390,7 +390,7 @@ var Args = class {
|
|
|
390
390
|
command: this.commands[0] || "",
|
|
391
391
|
flags: { ...this.flags },
|
|
392
392
|
options: { ...this.options },
|
|
393
|
-
usedKeys:
|
|
393
|
+
usedKeys: Array.from(this.usedKeys)
|
|
394
394
|
};
|
|
395
395
|
}
|
|
396
396
|
/**
|
|
@@ -606,8 +606,6 @@ var Params = class {
|
|
|
606
606
|
} else if (str.match(/^boolean|^bool/i)) {
|
|
607
607
|
type = Joi.boolean();
|
|
608
608
|
} else if (str.match(/^date/i)) {
|
|
609
|
-
type = Joi.date();
|
|
610
|
-
} else if (str.match(/^edate/i)) {
|
|
611
609
|
type = Joi.custom(joiEdateType);
|
|
612
610
|
} else if (str.match(/^duration/i)) {
|
|
613
611
|
type = Joi.string().isoDuration();
|
|
@@ -1603,11 +1601,11 @@ function buildBreadcrumb(parts) {
|
|
|
1603
1601
|
if (parts.length === 1) return parts[0];
|
|
1604
1602
|
return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
|
|
1605
1603
|
}
|
|
1606
|
-
function buildDetailBreadcrumb(
|
|
1607
|
-
if (
|
|
1608
|
-
return suffix ? `\u2190 ${suffix}` :
|
|
1604
|
+
function buildDetailBreadcrumb(path4, suffix = "") {
|
|
1605
|
+
if (path4.length <= 1) {
|
|
1606
|
+
return suffix ? `\u2190 ${suffix}` : path4[0] || "";
|
|
1609
1607
|
}
|
|
1610
|
-
const breadcrumb = buildBreadcrumb(
|
|
1608
|
+
const breadcrumb = buildBreadcrumb(path4);
|
|
1611
1609
|
return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
|
|
1612
1610
|
}
|
|
1613
1611
|
|
|
@@ -1715,9 +1713,899 @@ function organizeFooterMessages(messages) {
|
|
|
1715
1713
|
lines.push(...others);
|
|
1716
1714
|
return lines;
|
|
1717
1715
|
}
|
|
1716
|
+
|
|
1717
|
+
// src/filedatabase/index.ts
|
|
1718
|
+
import fs3 from "fs";
|
|
1719
|
+
import path3 from "path";
|
|
1720
|
+
|
|
1721
|
+
// src/utils/os-utils.ts
|
|
1722
|
+
import fs from "fs";
|
|
1723
|
+
import path from "path";
|
|
1724
|
+
import { execSync } from "child_process";
|
|
1725
|
+
function getFreeDiskSpace(targetPath) {
|
|
1726
|
+
try {
|
|
1727
|
+
let pathToCheck = targetPath;
|
|
1728
|
+
if (!fs.existsSync(targetPath)) {
|
|
1729
|
+
const parentDir = path.dirname(targetPath);
|
|
1730
|
+
if (fs.existsSync(parentDir)) {
|
|
1731
|
+
pathToCheck = parentDir;
|
|
1732
|
+
} else {
|
|
1733
|
+
pathToCheck = process.platform === "win32" ? "C:\\" : "/";
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
if (process.platform === "win32") {
|
|
1737
|
+
return null;
|
|
1738
|
+
} else {
|
|
1739
|
+
const stdout = execSync(`df -k "${pathToCheck}"`, { encoding: "utf8" });
|
|
1740
|
+
const lines = stdout.trim().split("\n");
|
|
1741
|
+
const parts = lines[1].split(/\s+/);
|
|
1742
|
+
const freeKb = parseInt(parts[3], 10);
|
|
1743
|
+
return freeKb * 1024;
|
|
1744
|
+
}
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
return null;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/utils/fs-utils.ts
|
|
1751
|
+
import fs2 from "fs";
|
|
1752
|
+
import path2 from "path";
|
|
1753
|
+
async function ensurePath(...pathParts) {
|
|
1754
|
+
const fullPath = path2.resolve(...pathParts);
|
|
1755
|
+
if (!fs2.existsSync(fullPath)) {
|
|
1756
|
+
await fs2.promises.mkdir(fullPath, { recursive: true });
|
|
1757
|
+
}
|
|
1758
|
+
return fullPath;
|
|
1759
|
+
}
|
|
1760
|
+
function getFileExtension(dataType) {
|
|
1761
|
+
switch (dataType) {
|
|
1762
|
+
case "json-array":
|
|
1763
|
+
case "json-object":
|
|
1764
|
+
return "json";
|
|
1765
|
+
case "text":
|
|
1766
|
+
return "txt";
|
|
1767
|
+
case "xml":
|
|
1768
|
+
return "xml";
|
|
1769
|
+
default:
|
|
1770
|
+
return "json";
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// src/utils/format-utils.ts
|
|
1775
|
+
function bytesToHumanReadable(bytes) {
|
|
1776
|
+
if (bytes === 0) return "0 B";
|
|
1777
|
+
const k = 1024;
|
|
1778
|
+
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
1779
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1780
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// src/utils/date-utils.ts
|
|
1784
|
+
function isTimestampFolder(folderName) {
|
|
1785
|
+
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
1786
|
+
if (!isoRegex.test(folderName)) {
|
|
1787
|
+
return false;
|
|
1788
|
+
}
|
|
1789
|
+
const date = new Date(folderName);
|
|
1790
|
+
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// src/filedatabase/serializers.ts
|
|
1794
|
+
function detectDataType(data) {
|
|
1795
|
+
if (Array.isArray(data)) {
|
|
1796
|
+
return "json-array";
|
|
1797
|
+
} else if (typeof data === "object" && data !== null) {
|
|
1798
|
+
return "json-object";
|
|
1799
|
+
} else if (typeof data === "string") {
|
|
1800
|
+
const trimmed = data.trim();
|
|
1801
|
+
if (trimmed.startsWith("<?xml") || trimmed.startsWith("<")) {
|
|
1802
|
+
return "xml";
|
|
1803
|
+
}
|
|
1804
|
+
return "text";
|
|
1805
|
+
} else {
|
|
1806
|
+
return "text";
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
function serializeData(data) {
|
|
1810
|
+
const dataType = detectDataType(data);
|
|
1811
|
+
if (dataType === "json-array" || dataType === "json-object") {
|
|
1812
|
+
return JSON.stringify(data, null, 4);
|
|
1813
|
+
} else {
|
|
1814
|
+
return String(data);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
function deserializeData(rawData, dataType) {
|
|
1818
|
+
if (dataType === "json-array" || dataType === "json-object") {
|
|
1819
|
+
return JSON.parse(rawData);
|
|
1820
|
+
} else {
|
|
1821
|
+
return rawData;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// src/filedatabase/synopsis-functions.ts
|
|
1826
|
+
function defaultFileSynopsisFunction(fileEntry, data) {
|
|
1827
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
1828
|
+
return { ...fileEntry };
|
|
1829
|
+
}
|
|
1830
|
+
const timestamps = [];
|
|
1831
|
+
const statusCounts = {};
|
|
1832
|
+
for (const item of data) {
|
|
1833
|
+
let ts = null;
|
|
1834
|
+
let status = null;
|
|
1835
|
+
for (const [key, value] of Object.entries(item)) {
|
|
1836
|
+
const k = key.toLowerCase();
|
|
1837
|
+
if (k === "modificationtimestamp") {
|
|
1838
|
+
ts = new Date(value).getTime();
|
|
1839
|
+
}
|
|
1840
|
+
if (k === "standardstatus") {
|
|
1841
|
+
status = value;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
if (ts && !isNaN(ts)) {
|
|
1845
|
+
timestamps.push(ts);
|
|
1846
|
+
}
|
|
1847
|
+
if (status !== null && status !== void 0) {
|
|
1848
|
+
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
const result = { ...fileEntry };
|
|
1852
|
+
if (timestamps.length) {
|
|
1853
|
+
result.minModificationTimestamp = new Date(Math.min(...timestamps)).toISOString();
|
|
1854
|
+
result.maxModificationTimestamp = new Date(Math.max(...timestamps)).toISOString();
|
|
1855
|
+
}
|
|
1856
|
+
if (Object.keys(statusCounts).length) {
|
|
1857
|
+
result.StandardStatuses = statusCounts;
|
|
1858
|
+
}
|
|
1859
|
+
return result;
|
|
1860
|
+
}
|
|
1861
|
+
function defaultVersionSynopsisFunction(metadata) {
|
|
1862
|
+
if (!metadata?.files || !Array.isArray(metadata.files)) {
|
|
1863
|
+
return metadata;
|
|
1864
|
+
}
|
|
1865
|
+
const timestamps = [];
|
|
1866
|
+
const statusCounts = {};
|
|
1867
|
+
for (const file of metadata.files) {
|
|
1868
|
+
if (file.minModificationTimestamp) {
|
|
1869
|
+
const minTs = new Date(file.minModificationTimestamp).getTime();
|
|
1870
|
+
if (!isNaN(minTs)) timestamps.push(minTs);
|
|
1871
|
+
}
|
|
1872
|
+
if (file.maxModificationTimestamp) {
|
|
1873
|
+
const maxTs = new Date(file.maxModificationTimestamp).getTime();
|
|
1874
|
+
if (!isNaN(maxTs)) timestamps.push(maxTs);
|
|
1875
|
+
}
|
|
1876
|
+
if (file.StandardStatuses && typeof file.StandardStatuses === "object") {
|
|
1877
|
+
for (const [status, count] of Object.entries(file.StandardStatuses)) {
|
|
1878
|
+
statusCounts[status] = (statusCounts[status] || 0) + count;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
const result = { ...metadata };
|
|
1883
|
+
if (timestamps.length) {
|
|
1884
|
+
result.minModificationTimestamp = new Date(Math.min(...timestamps)).toISOString();
|
|
1885
|
+
result.maxModificationTimestamp = new Date(Math.max(...timestamps)).toISOString();
|
|
1886
|
+
}
|
|
1887
|
+
if (Object.keys(statusCounts).length > 0) {
|
|
1888
|
+
result.StandardStatuses = statusCounts;
|
|
1889
|
+
}
|
|
1890
|
+
return result;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// src/filedatabase/index.ts
|
|
1894
|
+
var FileDatabaseError = class extends Error {
|
|
1895
|
+
constructor(message) {
|
|
1896
|
+
super(message);
|
|
1897
|
+
this.name = "FileDatabaseError";
|
|
1898
|
+
}
|
|
1899
|
+
};
|
|
1900
|
+
var FileDatabase = class {
|
|
1901
|
+
basePath;
|
|
1902
|
+
namespace;
|
|
1903
|
+
tableName = null;
|
|
1904
|
+
versioned;
|
|
1905
|
+
maxVersions;
|
|
1906
|
+
pageSize;
|
|
1907
|
+
useMetadata;
|
|
1908
|
+
freeSpaceThreshold;
|
|
1909
|
+
logger;
|
|
1910
|
+
// Current operation state
|
|
1911
|
+
currentVersion = null;
|
|
1912
|
+
currentVersionFolder = null;
|
|
1913
|
+
currentFileNumber = 0;
|
|
1914
|
+
currentRecord = 0;
|
|
1915
|
+
hasReadFirstPage = false;
|
|
1916
|
+
lastFileData = null;
|
|
1917
|
+
metadata;
|
|
1918
|
+
// Synopsis calculation functions
|
|
1919
|
+
fileSynopsisFunction = null;
|
|
1920
|
+
versionSynopsisFunction = null;
|
|
1921
|
+
constructor(config2) {
|
|
1922
|
+
if (!config2.basePath) {
|
|
1923
|
+
throw new ParamError("[FileDatabase] basePath is required");
|
|
1924
|
+
}
|
|
1925
|
+
this.basePath = config2.basePath;
|
|
1926
|
+
this.namespace = config2.namespace || "default";
|
|
1927
|
+
this.tableName = config2.tableName || null;
|
|
1928
|
+
this.versioned = config2.versioned ?? true;
|
|
1929
|
+
this.maxVersions = config2.maxVersions || 5;
|
|
1930
|
+
this.pageSize = config2.pageSize || 5e3;
|
|
1931
|
+
this.useMetadata = config2.useMetadata !== false;
|
|
1932
|
+
this.freeSpaceThreshold = config2.freeSpaceThreshold || 100 * 1024 * 1024;
|
|
1933
|
+
this.logger = config2.logger || console;
|
|
1934
|
+
this.metadata = this.getDefaultMetadata();
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Get default metadata structure
|
|
1938
|
+
*/
|
|
1939
|
+
getDefaultMetadata() {
|
|
1940
|
+
return {
|
|
1941
|
+
version: this.currentVersion || null,
|
|
1942
|
+
files: [],
|
|
1943
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1944
|
+
modifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1945
|
+
totalRecords: 0,
|
|
1946
|
+
synopsis: null,
|
|
1947
|
+
dataType: null
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Get the destination path (basePath/namespace/tableName[/version])
|
|
1952
|
+
*/
|
|
1953
|
+
getDestinationPath(version) {
|
|
1954
|
+
const errors = ["basePath", "namespace", "tableName"].filter((prop) => !this[prop]).map((prop) => `${prop} is not set`);
|
|
1955
|
+
if (errors.length) {
|
|
1956
|
+
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
1957
|
+
}
|
|
1958
|
+
let parts = [this.basePath, this.namespace];
|
|
1959
|
+
if (this.tableName) {
|
|
1960
|
+
parts.push(...this.tableName.split("/"));
|
|
1961
|
+
}
|
|
1962
|
+
if (this.versioned && version) {
|
|
1963
|
+
parts.push(version);
|
|
1964
|
+
}
|
|
1965
|
+
return path3.resolve(...parts);
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Set current version and version folder
|
|
1969
|
+
*/
|
|
1970
|
+
async setCurrentVersion(version) {
|
|
1971
|
+
this.currentVersion = version;
|
|
1972
|
+
this.currentVersionFolder = await ensurePath(this.getDestinationPath(), version);
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Create a new version folder with comprehensive timestamp logic
|
|
1976
|
+
* Only works in versioned mode
|
|
1977
|
+
*/
|
|
1978
|
+
async makeNewVersion() {
|
|
1979
|
+
if (!this.versioned) {
|
|
1980
|
+
throw new FileDatabaseError("makeNewVersion() only works in versioned mode");
|
|
1981
|
+
}
|
|
1982
|
+
this.metadata = this.getDefaultMetadata();
|
|
1983
|
+
const existingVersions = await this.getVersions();
|
|
1984
|
+
let versionName;
|
|
1985
|
+
if (existingVersions.length > 0) {
|
|
1986
|
+
const maxTimestamp = existingVersions.reduce((max, version) => {
|
|
1987
|
+
const versionDate = new Date(version.replace("Z", ""));
|
|
1988
|
+
const maxDate2 = new Date(max.replace("Z", ""));
|
|
1989
|
+
return versionDate > maxDate2 ? version : max;
|
|
1990
|
+
});
|
|
1991
|
+
const maxDate = new Date(maxTimestamp.replace("Z", ""));
|
|
1992
|
+
const nextDate = new Date(maxDate.getTime() + 1e3);
|
|
1993
|
+
versionName = nextDate.toISOString().split(".")[0] + "Z";
|
|
1994
|
+
} else {
|
|
1995
|
+
const now = /* @__PURE__ */ new Date();
|
|
1996
|
+
versionName = now.toISOString().split(".")[0] + "Z";
|
|
1997
|
+
}
|
|
1998
|
+
await this.setCurrentVersion(versionName);
|
|
1999
|
+
this.currentFileNumber = 0;
|
|
2000
|
+
const versions = await this.getVersions();
|
|
2001
|
+
while (versions.length > this.maxVersions) {
|
|
2002
|
+
const versionToDelete = path3.resolve(this.getDestinationPath(), versions.shift());
|
|
2003
|
+
this.logger.debug?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
|
|
2004
|
+
await fs3.promises.rm(versionToDelete, { recursive: true, force: true });
|
|
2005
|
+
}
|
|
2006
|
+
return versionName;
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Get list of all versions (sorted chronologically)
|
|
2010
|
+
* Only works in versioned mode
|
|
2011
|
+
*/
|
|
2012
|
+
async getVersions() {
|
|
2013
|
+
if (!this.versioned) {
|
|
2014
|
+
return [];
|
|
2015
|
+
}
|
|
2016
|
+
const destPath = this.getDestinationPath();
|
|
2017
|
+
try {
|
|
2018
|
+
await ensurePath(destPath);
|
|
2019
|
+
const items = await fs3.promises.readdir(destPath);
|
|
2020
|
+
const versions = items.filter((item) => {
|
|
2021
|
+
const itemPath = path3.join(destPath, item);
|
|
2022
|
+
const stat = fs3.statSync(itemPath);
|
|
2023
|
+
return stat.isDirectory() && isTimestampFolder(item);
|
|
2024
|
+
});
|
|
2025
|
+
return versions.sort();
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
return [];
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Get the latest version (most recent timestamp)
|
|
2032
|
+
* Only works in versioned mode
|
|
2033
|
+
* @returns Latest version string or null if no versions
|
|
2034
|
+
*/
|
|
2035
|
+
async getLatestVersion() {
|
|
2036
|
+
if (!this.versioned) {
|
|
2037
|
+
throw new FileDatabaseError("getLatestVersion() only works in versioned mode");
|
|
2038
|
+
}
|
|
2039
|
+
const versions = await this.getVersions();
|
|
2040
|
+
if (versions.length === 0) {
|
|
2041
|
+
return null;
|
|
2042
|
+
}
|
|
2043
|
+
return versions[versions.length - 1];
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Check if any data exists in this table
|
|
2047
|
+
* Works for both versioned and non-versioned modes
|
|
2048
|
+
* @returns true if data exists
|
|
2049
|
+
*/
|
|
2050
|
+
async hasData() {
|
|
2051
|
+
const tablePath = this.getDestinationPath();
|
|
2052
|
+
if (!fs3.existsSync(tablePath)) {
|
|
2053
|
+
return false;
|
|
2054
|
+
}
|
|
2055
|
+
if (this.versioned) {
|
|
2056
|
+
const versions = await this.getVersions();
|
|
2057
|
+
return versions.length > 0;
|
|
2058
|
+
} else {
|
|
2059
|
+
const items = await fs3.promises.readdir(tablePath);
|
|
2060
|
+
return items.some(
|
|
2061
|
+
(item) => item === "metadata.json" || item.match(/^\d{6}\.(json|txt|xml)$/) || item.endsWith(".json")
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Auto-detect the data format in this table
|
|
2067
|
+
* Used when reading existing data
|
|
2068
|
+
* @returns Format detection result
|
|
2069
|
+
*/
|
|
2070
|
+
async detectDataFormat() {
|
|
2071
|
+
const tablePath = this.getDestinationPath();
|
|
2072
|
+
if (!fs3.existsSync(tablePath)) {
|
|
2073
|
+
return { versioned: false, hasMetadata: false, dataType: null };
|
|
2074
|
+
}
|
|
2075
|
+
const items = await fs3.promises.readdir(tablePath);
|
|
2076
|
+
if (items.includes("metadata.json")) {
|
|
2077
|
+
const metadata = JSON.parse(
|
|
2078
|
+
await fs3.promises.readFile(path3.join(tablePath, "metadata.json"), "utf8")
|
|
2079
|
+
);
|
|
2080
|
+
return {
|
|
2081
|
+
versioned: false,
|
|
2082
|
+
hasMetadata: true,
|
|
2083
|
+
dataType: metadata.dataType || null
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
const versionFolders = items.filter((item) => {
|
|
2087
|
+
const itemPath = path3.join(tablePath, item);
|
|
2088
|
+
const stat = fs3.statSync(itemPath);
|
|
2089
|
+
return stat.isDirectory() && isTimestampFolder(item);
|
|
2090
|
+
});
|
|
2091
|
+
if (versionFolders.length > 0) {
|
|
2092
|
+
const latestVersion = versionFolders.sort().pop();
|
|
2093
|
+
const versionMetadataPath = path3.join(tablePath, latestVersion, "metadata.json");
|
|
2094
|
+
return {
|
|
2095
|
+
versioned: true,
|
|
2096
|
+
hasMetadata: fs3.existsSync(versionMetadataPath),
|
|
2097
|
+
dataType: null
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
const dataFiles = items.filter((f) => f.match(/^\d{6}\.(json|txt|xml)$/));
|
|
2101
|
+
if (dataFiles.length > 0) {
|
|
2102
|
+
return {
|
|
2103
|
+
versioned: false,
|
|
2104
|
+
hasMetadata: false,
|
|
2105
|
+
dataType: null
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
return { versioned: false, hasMetadata: false, dataType: null };
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Load metadata from JSON file
|
|
2112
|
+
*/
|
|
2113
|
+
async loadMetadataJson(version) {
|
|
2114
|
+
const metadataFile = path3.join(this.getDestinationPath(), version, "metadata.json");
|
|
2115
|
+
if (fs3.existsSync(metadataFile)) {
|
|
2116
|
+
try {
|
|
2117
|
+
const rawData = await fs3.promises.readFile(metadataFile, "utf8");
|
|
2118
|
+
return JSON.parse(rawData);
|
|
2119
|
+
} catch (e) {
|
|
2120
|
+
throw new FileDatabaseError(`Failed to read metadata for version "${version}": ${e.message}`);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
return null;
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Build metadata by scanning files in a version folder (backward compatibility)
|
|
2127
|
+
* Reads all files to get accurate counts - used when synopsis calculation is needed
|
|
2128
|
+
*/
|
|
2129
|
+
async figureMetadataFromVersionFiles(version) {
|
|
2130
|
+
const versionPath = path3.join(this.getDestinationPath(), version);
|
|
2131
|
+
if (!fs3.existsSync(versionPath)) {
|
|
2132
|
+
return this.getDefaultMetadata();
|
|
2133
|
+
}
|
|
2134
|
+
const files = (await fs3.promises.readdir(versionPath)).filter((file) => file !== "metadata.json" && !file.startsWith(".")).sort();
|
|
2135
|
+
const metadata = this.getDefaultMetadata();
|
|
2136
|
+
metadata.version = version;
|
|
2137
|
+
metadata.files = [];
|
|
2138
|
+
let totalRecords = 0;
|
|
2139
|
+
let detectedDataType = null;
|
|
2140
|
+
for (let i = 0; i < files.length; i++) {
|
|
2141
|
+
const fileName = files[i];
|
|
2142
|
+
const filePath = path3.join(versionPath, fileName);
|
|
2143
|
+
try {
|
|
2144
|
+
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
2145
|
+
const extension = path3.extname(fileName).toLowerCase();
|
|
2146
|
+
let dataType = "text";
|
|
2147
|
+
if (extension === ".json") {
|
|
2148
|
+
dataType = "json-array";
|
|
2149
|
+
} else if (extension === ".xml") {
|
|
2150
|
+
dataType = "xml";
|
|
2151
|
+
}
|
|
2152
|
+
const fileData = deserializeData(rawData, dataType);
|
|
2153
|
+
const recordsCount = Array.isArray(fileData) ? fileData.length : 1;
|
|
2154
|
+
if (detectedDataType === null) {
|
|
2155
|
+
detectedDataType = detectDataType(fileData);
|
|
2156
|
+
}
|
|
2157
|
+
const fileInfo = {
|
|
2158
|
+
number: i + 1,
|
|
2159
|
+
recordsCount,
|
|
2160
|
+
fileName
|
|
2161
|
+
};
|
|
2162
|
+
metadata.files.push(fileInfo);
|
|
2163
|
+
totalRecords += recordsCount;
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
this.logger.error?.(`[FileDatabase] Failed to read file ${fileName}: ${error.message}`);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
metadata.totalRecords = totalRecords;
|
|
2169
|
+
metadata.dataType = detectedDataType;
|
|
2170
|
+
return metadata;
|
|
2171
|
+
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Build metadata optimized - only reads first and last files
|
|
2174
|
+
* Assumes all middle files have the same record count as the first file
|
|
2175
|
+
* Much faster for large datasets with many files
|
|
2176
|
+
*/
|
|
2177
|
+
async buildMetadataOptimized(version) {
|
|
2178
|
+
const versionPath = path3.join(this.getDestinationPath(), version);
|
|
2179
|
+
if (!fs3.existsSync(versionPath)) {
|
|
2180
|
+
return this.getDefaultMetadata();
|
|
2181
|
+
}
|
|
2182
|
+
const files = (await fs3.promises.readdir(versionPath)).filter((file) => file !== "metadata.json" && !file.startsWith(".")).sort();
|
|
2183
|
+
if (files.length === 0) {
|
|
2184
|
+
return this.getDefaultMetadata();
|
|
2185
|
+
}
|
|
2186
|
+
const metadata = this.getDefaultMetadata();
|
|
2187
|
+
metadata.version = version;
|
|
2188
|
+
metadata.files = files.map((fileName, index) => ({
|
|
2189
|
+
number: index + 1,
|
|
2190
|
+
recordsCount: 0,
|
|
2191
|
+
fileName
|
|
2192
|
+
}));
|
|
2193
|
+
const firstFile = metadata.files[0];
|
|
2194
|
+
const firstFilePath = path3.join(versionPath, firstFile.fileName);
|
|
2195
|
+
const firstFileRaw = await fs3.promises.readFile(firstFilePath, "utf8");
|
|
2196
|
+
let firstFileData;
|
|
2197
|
+
try {
|
|
2198
|
+
firstFileData = JSON.parse(firstFileRaw);
|
|
2199
|
+
} catch (e) {
|
|
2200
|
+
firstFileData = firstFileRaw;
|
|
2201
|
+
}
|
|
2202
|
+
metadata.dataType = detectDataType(firstFileData);
|
|
2203
|
+
if (metadata.dataType === "json-array") {
|
|
2204
|
+
const firstFileCount = Array.isArray(firstFileData) ? firstFileData.length : 1;
|
|
2205
|
+
firstFile.recordsCount = firstFileCount;
|
|
2206
|
+
for (let i = 1; i < metadata.files.length - 1; i++) {
|
|
2207
|
+
metadata.files[i].recordsCount = firstFileCount;
|
|
2208
|
+
}
|
|
2209
|
+
if (files.length > 1) {
|
|
2210
|
+
const lastFile = metadata.files[metadata.files.length - 1];
|
|
2211
|
+
const lastFilePath = path3.join(versionPath, lastFile.fileName);
|
|
2212
|
+
const lastFileRaw = await fs3.promises.readFile(lastFilePath, "utf8");
|
|
2213
|
+
const lastFileData = deserializeData(lastFileRaw, metadata.dataType);
|
|
2214
|
+
lastFile.recordsCount = Array.isArray(lastFileData) ? lastFileData.length : 1;
|
|
2215
|
+
}
|
|
2216
|
+
metadata.totalRecords = metadata.files.reduce((sum, file) => sum + file.recordsCount, 0);
|
|
2217
|
+
} else {
|
|
2218
|
+
metadata.files.forEach((file) => {
|
|
2219
|
+
file.recordsCount = 1;
|
|
2220
|
+
});
|
|
2221
|
+
metadata.totalRecords = files.length;
|
|
2222
|
+
}
|
|
2223
|
+
return metadata;
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Figure out metadata - tries JSON first, then builds from files
|
|
2227
|
+
* Uses optimized building when no synopsis calculation is needed
|
|
2228
|
+
*/
|
|
2229
|
+
async figureMetadata(version, useOptimized = true) {
|
|
2230
|
+
if (this.useMetadata) {
|
|
2231
|
+
const metadata = await this.loadMetadataJson(version);
|
|
2232
|
+
if (metadata) {
|
|
2233
|
+
return metadata;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
if (useOptimized && !this.fileSynopsisFunction && !this.versionSynopsisFunction) {
|
|
2237
|
+
return await this.buildMetadataOptimized(version);
|
|
2238
|
+
}
|
|
2239
|
+
return await this.figureMetadataFromVersionFiles(version);
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Load version metadata (main entry point for loading)
|
|
2243
|
+
*/
|
|
2244
|
+
async loadVersionMetadata(version) {
|
|
2245
|
+
const metadata = await this.figureMetadata(version);
|
|
2246
|
+
this.metadata = metadata;
|
|
2247
|
+
return metadata;
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Save version metadata to file
|
|
2251
|
+
*/
|
|
2252
|
+
async saveVersionMetadata(metadata) {
|
|
2253
|
+
if (!this.useMetadata) {
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
const metadataToSave = metadata || this.metadata;
|
|
2257
|
+
let metadataFile;
|
|
2258
|
+
if (this.versioned) {
|
|
2259
|
+
if (!this.currentVersion) {
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
metadataFile = path3.join(this.getDestinationPath(), this.currentVersion, "metadata.json");
|
|
2263
|
+
} else {
|
|
2264
|
+
metadataFile = path3.join(this.getDestinationPath(), "metadata.json");
|
|
2265
|
+
}
|
|
2266
|
+
await fs3.promises.writeFile(metadataFile, JSON.stringify(metadataToSave, null, 4), "utf8");
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Create a new file entry in metadata
|
|
2270
|
+
*/
|
|
2271
|
+
makeNewFile() {
|
|
2272
|
+
this.currentFileNumber = (this.currentFileNumber || 0) + 1;
|
|
2273
|
+
const dataType = this.metadata.dataType || "json-array";
|
|
2274
|
+
const fileEntry = {
|
|
2275
|
+
number: this.currentFileNumber,
|
|
2276
|
+
recordsCount: 0,
|
|
2277
|
+
fileName: `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(dataType)}`
|
|
2278
|
+
};
|
|
2279
|
+
this.metadata.files.push(fileEntry);
|
|
2280
|
+
this.lastFileData = null;
|
|
2281
|
+
this.logger.debug?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Figure out what data to write and which file to use (for pagination)
|
|
2285
|
+
*/
|
|
2286
|
+
figureOutDataAndFileToWrite(data) {
|
|
2287
|
+
let dataToWrite;
|
|
2288
|
+
let dataLeftOver;
|
|
2289
|
+
const lastFile = this.metadata.files[this.metadata.files.length - 1];
|
|
2290
|
+
const lastFileRecordsCount = lastFile.recordsCount;
|
|
2291
|
+
if (Array.isArray(data)) {
|
|
2292
|
+
if (lastFileRecordsCount < this.pageSize) {
|
|
2293
|
+
dataToWrite = [...this.lastFileData || [], ...data.slice(0, this.pageSize - lastFileRecordsCount)];
|
|
2294
|
+
dataLeftOver = data.slice(this.pageSize - lastFileRecordsCount);
|
|
2295
|
+
} else {
|
|
2296
|
+
this.makeNewFile();
|
|
2297
|
+
dataToWrite = data.slice(0, this.pageSize);
|
|
2298
|
+
dataLeftOver = data.slice(this.pageSize);
|
|
2299
|
+
}
|
|
2300
|
+
this.lastFileData = dataToWrite;
|
|
2301
|
+
} else {
|
|
2302
|
+
dataToWrite = data;
|
|
2303
|
+
dataLeftOver = null;
|
|
2304
|
+
}
|
|
2305
|
+
const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
|
|
2306
|
+
this.logger.debug?.(
|
|
2307
|
+
`[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
|
|
2308
|
+
);
|
|
2309
|
+
return { dataToWrite, dataLeftOver, fileName };
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* Calculate file-level synopsis if function is set
|
|
2313
|
+
*/
|
|
2314
|
+
calculateFileSynopsis(data, fileIndex = this.metadata.files.length - 1) {
|
|
2315
|
+
if (!this.fileSynopsisFunction) {
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
const fileInfo = this.metadata.files[fileIndex];
|
|
2319
|
+
const enhancedFileInfo = this.fileSynopsisFunction(fileInfo, data);
|
|
2320
|
+
this.metadata.files[fileIndex] = enhancedFileInfo;
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Calculate version-level synopsis if function is set
|
|
2324
|
+
*/
|
|
2325
|
+
calculateVersionSynopsis() {
|
|
2326
|
+
if (!this.versionSynopsisFunction) {
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
const enhancedMetadata = this.versionSynopsisFunction(this.metadata);
|
|
2330
|
+
this.metadata = enhancedMetadata;
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Update metadata after writing data
|
|
2334
|
+
*/
|
|
2335
|
+
updateMetadata(dataToWrite, fileName) {
|
|
2336
|
+
let currentFile;
|
|
2337
|
+
if (fileName) {
|
|
2338
|
+
const foundFile = this.metadata.files.find((file) => file.fileName === fileName);
|
|
2339
|
+
if (!foundFile) {
|
|
2340
|
+
this.logger.warn?.(`[FileDatabase] File ${fileName} not found in metadata, using last file`);
|
|
2341
|
+
currentFile = this.metadata.files[this.metadata.files.length - 1];
|
|
2342
|
+
} else {
|
|
2343
|
+
currentFile = foundFile;
|
|
2344
|
+
}
|
|
2345
|
+
} else {
|
|
2346
|
+
currentFile = this.metadata.files[this.metadata.files.length - 1];
|
|
2347
|
+
}
|
|
2348
|
+
const recordsCount = Array.isArray(dataToWrite) ? dataToWrite.length : 1;
|
|
2349
|
+
currentFile.recordsCount = recordsCount;
|
|
2350
|
+
const fileIndex = this.metadata.files.indexOf(currentFile);
|
|
2351
|
+
if (fileIndex !== -1) {
|
|
2352
|
+
this.calculateFileSynopsis(dataToWrite, fileIndex);
|
|
2353
|
+
}
|
|
2354
|
+
this.metadata.version = this.currentVersion;
|
|
2355
|
+
this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2356
|
+
this.metadata.dataType = detectDataType(dataToWrite);
|
|
2357
|
+
this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
|
|
2358
|
+
this.logger.debug?.(
|
|
2359
|
+
`[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* Safe write with disk space check
|
|
2364
|
+
*/
|
|
2365
|
+
async safeWrite(filePath, data) {
|
|
2366
|
+
const serializedData = serializeData(data);
|
|
2367
|
+
const dir = path3.dirname(filePath);
|
|
2368
|
+
const requiredBytes = Buffer.byteLength(serializedData, "utf8");
|
|
2369
|
+
const freeBytes = getFreeDiskSpace(dir);
|
|
2370
|
+
if (freeBytes !== null) {
|
|
2371
|
+
if (freeBytes < requiredBytes) {
|
|
2372
|
+
throw new FileDatabaseError(
|
|
2373
|
+
`Not enough disk space. Required: ${bytesToHumanReadable(requiredBytes)}, Free: ${bytesToHumanReadable(freeBytes)}`
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
if (freeBytes < this.freeSpaceThreshold) {
|
|
2377
|
+
this.logger.warn?.(`Low disk space warning: only ${bytesToHumanReadable(freeBytes)} left`);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
try {
|
|
2381
|
+
await fs3.promises.writeFile(filePath, serializedData, "utf8");
|
|
2382
|
+
this.logger.debug?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
|
|
2383
|
+
} catch (error) {
|
|
2384
|
+
throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
/**
|
|
2388
|
+
* Prepare the instance for read or write operations
|
|
2389
|
+
* This discovers state and sets up internal members based on mode and current data
|
|
2390
|
+
*/
|
|
2391
|
+
async prepare({ write, read, version }) {
|
|
2392
|
+
if (write) {
|
|
2393
|
+
if (this.versioned) {
|
|
2394
|
+
if (this.currentVersion === null) {
|
|
2395
|
+
await this.makeNewVersion();
|
|
2396
|
+
this.metadata = this.getDefaultMetadata();
|
|
2397
|
+
this.metadata.version = this.currentVersion;
|
|
2398
|
+
this.makeNewFile();
|
|
2399
|
+
} else {
|
|
2400
|
+
if (!this.metadata.files.length) {
|
|
2401
|
+
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
} else {
|
|
2405
|
+
await ensurePath(this.getDestinationPath());
|
|
2406
|
+
if (this.useMetadata === true) {
|
|
2407
|
+
const metadataPath = path3.join(this.getDestinationPath(), "metadata.json");
|
|
2408
|
+
if (fs3.existsSync(metadataPath)) {
|
|
2409
|
+
try {
|
|
2410
|
+
const rawData = await fs3.promises.readFile(metadataPath, "utf8");
|
|
2411
|
+
this.metadata = JSON.parse(rawData);
|
|
2412
|
+
} catch (e) {
|
|
2413
|
+
this.metadata = this.getDefaultMetadata();
|
|
2414
|
+
}
|
|
2415
|
+
} else {
|
|
2416
|
+
this.metadata = this.getDefaultMetadata();
|
|
2417
|
+
this.makeNewFile();
|
|
2418
|
+
}
|
|
2419
|
+
} else {
|
|
2420
|
+
this.metadata = this.getDefaultMetadata();
|
|
2421
|
+
this.makeNewFile();
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
} else if (read) {
|
|
2425
|
+
if (this.versioned) {
|
|
2426
|
+
const versions = await this.getVersions();
|
|
2427
|
+
if (versions.length === 0) {
|
|
2428
|
+
throw new FileDatabaseError("[FileDatabase] No versions found, cannot read");
|
|
2429
|
+
}
|
|
2430
|
+
if (version) {
|
|
2431
|
+
if (!versions.includes(version)) {
|
|
2432
|
+
throw new FileDatabaseError(`[FileDatabase] Version "${version}" not found`);
|
|
2433
|
+
}
|
|
2434
|
+
await this.setCurrentVersion(version);
|
|
2435
|
+
} else {
|
|
2436
|
+
await this.setCurrentVersion(versions[versions.length - 1]);
|
|
2437
|
+
}
|
|
2438
|
+
if (!this.metadata.files.length) {
|
|
2439
|
+
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
2440
|
+
}
|
|
2441
|
+
} else {
|
|
2442
|
+
this.currentVersion = null;
|
|
2443
|
+
if (this.useMetadata === void 0) {
|
|
2444
|
+
const format = await this.detectDataFormat();
|
|
2445
|
+
this.useMetadata = format.hasMetadata;
|
|
2446
|
+
}
|
|
2447
|
+
if (this.useMetadata) {
|
|
2448
|
+
const metadataPath = path3.join(this.getDestinationPath(), "metadata.json");
|
|
2449
|
+
if (fs3.existsSync(metadataPath)) {
|
|
2450
|
+
try {
|
|
2451
|
+
const rawData = await fs3.promises.readFile(metadataPath, "utf8");
|
|
2452
|
+
this.metadata = JSON.parse(rawData);
|
|
2453
|
+
} catch (e) {
|
|
2454
|
+
throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
|
|
2455
|
+
}
|
|
2456
|
+
} else {
|
|
2457
|
+
throw new FileDatabaseError("[FileDatabase] No metadata found in non-versioned mode");
|
|
2458
|
+
}
|
|
2459
|
+
} else {
|
|
2460
|
+
this.metadata = await this.figureMetadataFromVersionFiles("");
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Write data to the file database
|
|
2467
|
+
*/
|
|
2468
|
+
async write(data, options = {}) {
|
|
2469
|
+
if (options.forceNewVersion && !this.versioned) {
|
|
2470
|
+
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
2471
|
+
}
|
|
2472
|
+
await this.prepare({ write: true });
|
|
2473
|
+
if (options.forceNewVersion) {
|
|
2474
|
+
await this.makeNewVersion();
|
|
2475
|
+
this.metadata = this.getDefaultMetadata();
|
|
2476
|
+
this.metadata.version = this.currentVersion;
|
|
2477
|
+
this.makeNewFile();
|
|
2478
|
+
}
|
|
2479
|
+
let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data);
|
|
2480
|
+
const destPath = this.getDestinationPath(this.currentVersion || void 0);
|
|
2481
|
+
await this.safeWrite(path3.join(destPath, fileName), dataToWrite);
|
|
2482
|
+
this.updateMetadata(dataToWrite, fileName);
|
|
2483
|
+
while (dataLeftOver && dataLeftOver.length > 0) {
|
|
2484
|
+
const writeContext = this.figureOutDataAndFileToWrite(dataLeftOver);
|
|
2485
|
+
await this.safeWrite(path3.join(destPath, writeContext.fileName), writeContext.dataToWrite);
|
|
2486
|
+
this.updateMetadata(writeContext.dataToWrite, writeContext.fileName);
|
|
2487
|
+
dataLeftOver = writeContext.dataLeftOver;
|
|
2488
|
+
}
|
|
2489
|
+
this.calculateVersionSynopsis();
|
|
2490
|
+
if (this.useMetadata) {
|
|
2491
|
+
await this.saveVersionMetadata(this.metadata);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* Read data from the file database
|
|
2496
|
+
*/
|
|
2497
|
+
async read(options = {}) {
|
|
2498
|
+
const { version, nextPage = false, pageSize } = options;
|
|
2499
|
+
await this.prepare({ read: true, version });
|
|
2500
|
+
const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
|
|
2501
|
+
if (isNonPaginatedData) {
|
|
2502
|
+
const file = this.metadata.files[0];
|
|
2503
|
+
const filePath = path3.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
|
|
2504
|
+
try {
|
|
2505
|
+
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
2506
|
+
return deserializeData(rawData, this.metadata.dataType);
|
|
2507
|
+
} catch (error) {
|
|
2508
|
+
throw new FileDatabaseError(`Failed to read file ${file.fileName}: ${error.message}`);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
let effectivePageSize;
|
|
2512
|
+
if (nextPage && this.hasReadFirstPage) {
|
|
2513
|
+
effectivePageSize = pageSize || this.pageSize;
|
|
2514
|
+
this.currentRecord += effectivePageSize;
|
|
2515
|
+
} else if (!nextPage) {
|
|
2516
|
+
effectivePageSize = pageSize !== void 0 ? pageSize : this.metadata.totalRecords;
|
|
2517
|
+
this.currentRecord = 0;
|
|
2518
|
+
} else {
|
|
2519
|
+
effectivePageSize = pageSize || this.pageSize;
|
|
2520
|
+
}
|
|
2521
|
+
if (this.currentRecord >= this.metadata.totalRecords) {
|
|
2522
|
+
return [];
|
|
2523
|
+
}
|
|
2524
|
+
const result = [];
|
|
2525
|
+
let recordsRead = 0;
|
|
2526
|
+
let currentFileIndex = 0;
|
|
2527
|
+
let currentFileOffset = 0;
|
|
2528
|
+
let totalRecords = 0;
|
|
2529
|
+
for (let i = 0; i < this.metadata.files.length; i++) {
|
|
2530
|
+
const file = this.metadata.files[i];
|
|
2531
|
+
if (this.currentRecord < totalRecords + file.recordsCount) {
|
|
2532
|
+
currentFileIndex = i;
|
|
2533
|
+
currentFileOffset = totalRecords;
|
|
2534
|
+
break;
|
|
2535
|
+
}
|
|
2536
|
+
totalRecords += file.recordsCount;
|
|
2537
|
+
}
|
|
2538
|
+
let cumulativeRecords = currentFileOffset;
|
|
2539
|
+
for (let i = currentFileIndex; i < this.metadata.files.length && recordsRead < effectivePageSize; i++) {
|
|
2540
|
+
const file = this.metadata.files[i];
|
|
2541
|
+
const filePath = path3.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
|
|
2542
|
+
try {
|
|
2543
|
+
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
2544
|
+
const fileData = deserializeData(rawData, this.metadata.dataType);
|
|
2545
|
+
let startIndex = 0;
|
|
2546
|
+
if (i === currentFileIndex) {
|
|
2547
|
+
startIndex = this.currentRecord - cumulativeRecords;
|
|
2548
|
+
}
|
|
2549
|
+
const endIndex = Math.min(startIndex + (effectivePageSize - recordsRead), fileData.length);
|
|
2550
|
+
const recordsFromThisFile = fileData.slice(startIndex, endIndex);
|
|
2551
|
+
result.push(...recordsFromThisFile);
|
|
2552
|
+
recordsRead += recordsFromThisFile.length;
|
|
2553
|
+
cumulativeRecords += file.recordsCount;
|
|
2554
|
+
} catch (error) {
|
|
2555
|
+
throw new FileDatabaseError(`Failed to read file ${file.fileName}: ${error.message}`);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
if (result.length > 0) {
|
|
2559
|
+
if (nextPage || pageSize !== void 0 && pageSize < this.metadata.totalRecords) {
|
|
2560
|
+
this.hasReadFirstPage = true;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
return result;
|
|
2564
|
+
}
|
|
2565
|
+
/**
|
|
2566
|
+
* Set the starting record for pagination (1-based index)
|
|
2567
|
+
*/
|
|
2568
|
+
setStartRecord(startRecord) {
|
|
2569
|
+
this.currentRecord = startRecord - 1;
|
|
2570
|
+
this.hasReadFirstPage = false;
|
|
2571
|
+
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Reset read pagination state
|
|
2574
|
+
*/
|
|
2575
|
+
resetPagination() {
|
|
2576
|
+
this.currentRecord = 0;
|
|
2577
|
+
this.hasReadFirstPage = false;
|
|
2578
|
+
}
|
|
2579
|
+
/**
|
|
2580
|
+
* Set file-level synopsis calculation function
|
|
2581
|
+
*/
|
|
2582
|
+
setFileSynopsisFunction(fn) {
|
|
2583
|
+
this.fileSynopsisFunction = fn;
|
|
2584
|
+
}
|
|
2585
|
+
/**
|
|
2586
|
+
* Set version-level synopsis calculation function
|
|
2587
|
+
*/
|
|
2588
|
+
setVersionSynopsisFunction(fn) {
|
|
2589
|
+
this.versionSynopsisFunction = fn;
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* Get current version name
|
|
2593
|
+
*/
|
|
2594
|
+
getCurrentVersion() {
|
|
2595
|
+
return this.currentVersion;
|
|
2596
|
+
}
|
|
2597
|
+
/**
|
|
2598
|
+
* Get current metadata
|
|
2599
|
+
*/
|
|
2600
|
+
getMetadata() {
|
|
2601
|
+
return { ...this.metadata };
|
|
2602
|
+
}
|
|
2603
|
+
};
|
|
1718
2604
|
export {
|
|
1719
2605
|
Args,
|
|
1720
2606
|
Divider,
|
|
2607
|
+
FileDatabase,
|
|
2608
|
+
FileDatabaseError,
|
|
1721
2609
|
FooterPresets,
|
|
1722
2610
|
GridCell,
|
|
1723
2611
|
InputField,
|
|
@@ -1736,6 +2624,8 @@ export {
|
|
|
1736
2624
|
buildBreadcrumb,
|
|
1737
2625
|
buildDetailBreadcrumb,
|
|
1738
2626
|
buildFooter,
|
|
2627
|
+
defaultFileSynopsisFunction,
|
|
2628
|
+
defaultVersionSynopsisFunction,
|
|
1739
2629
|
getArgsInstance,
|
|
1740
2630
|
getParamsInstance,
|
|
1741
2631
|
joiEdateType,
|