@nmakarov/cli-toolkit 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -32,6 +32,8 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  Args: () => Args,
34
34
  Divider: () => Divider,
35
+ FileDatabase: () => FileDatabase,
36
+ FileDatabaseError: () => FileDatabaseError,
35
37
  FooterPresets: () => FooterPresets,
36
38
  GridCell: () => GridCell,
37
39
  InputField: () => InputField,
@@ -50,6 +52,8 @@ __export(src_exports, {
50
52
  buildBreadcrumb: () => buildBreadcrumb,
51
53
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
52
54
  buildFooter: () => buildFooter,
55
+ defaultFileSynopsisFunction: () => defaultFileSynopsisFunction,
56
+ defaultVersionSynopsisFunction: () => defaultVersionSynopsisFunction,
53
57
  getArgsInstance: () => getArgsInstance,
54
58
  getParamsInstance: () => getParamsInstance,
55
59
  joiEdateType: () => joiEdateType,
@@ -449,7 +453,7 @@ var Args = class {
449
453
  command: this.commands[0] || "",
450
454
  flags: { ...this.flags },
451
455
  options: { ...this.options },
452
- usedKeys: new Set(this.usedKeys)
456
+ usedKeys: Array.from(this.usedKeys)
453
457
  };
454
458
  }
455
459
  /**
@@ -665,8 +669,6 @@ var Params = class {
665
669
  } else if (str.match(/^boolean|^bool/i)) {
666
670
  type = import_joi.default.boolean();
667
671
  } else if (str.match(/^date/i)) {
668
- type = import_joi.default.date();
669
- } else if (str.match(/^edate/i)) {
670
672
  type = import_joi.default.custom(joiEdateType);
671
673
  } else if (str.match(/^duration/i)) {
672
674
  type = import_joi.default.string().isoDuration();
@@ -1662,11 +1664,11 @@ function buildBreadcrumb(parts) {
1662
1664
  if (parts.length === 1) return parts[0];
1663
1665
  return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
1664
1666
  }
1665
- function buildDetailBreadcrumb(path, suffix = "") {
1666
- if (path.length <= 1) {
1667
- return suffix ? `\u2190 ${suffix}` : path[0] || "";
1667
+ function buildDetailBreadcrumb(path4, suffix = "") {
1668
+ if (path4.length <= 1) {
1669
+ return suffix ? `\u2190 ${suffix}` : path4[0] || "";
1668
1670
  }
1669
- const breadcrumb = buildBreadcrumb(path);
1671
+ const breadcrumb = buildBreadcrumb(path4);
1670
1672
  return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
1671
1673
  }
1672
1674
 
@@ -1774,10 +1776,746 @@ function organizeFooterMessages(messages) {
1774
1776
  lines.push(...others);
1775
1777
  return lines;
1776
1778
  }
1779
+
1780
+ // src/filestore/index.ts
1781
+ var import_fs4 = __toESM(require("fs"), 1);
1782
+ var import_path4 = __toESM(require("path"), 1);
1783
+
1784
+ // src/utils/os-utils.ts
1785
+ var import_fs2 = __toESM(require("fs"), 1);
1786
+ var import_path2 = __toESM(require("path"), 1);
1787
+ var import_child_process = require("child_process");
1788
+ function getFreeDiskSpace(targetPath) {
1789
+ try {
1790
+ let pathToCheck = targetPath;
1791
+ if (!import_fs2.default.existsSync(targetPath)) {
1792
+ const parentDir = import_path2.default.dirname(targetPath);
1793
+ if (import_fs2.default.existsSync(parentDir)) {
1794
+ pathToCheck = parentDir;
1795
+ } else {
1796
+ pathToCheck = process.platform === "win32" ? "C:\\" : "/";
1797
+ }
1798
+ }
1799
+ if (process.platform === "win32") {
1800
+ return null;
1801
+ } else {
1802
+ const stdout = (0, import_child_process.execSync)(`df -k "${pathToCheck}"`, { encoding: "utf8" });
1803
+ const lines = stdout.trim().split("\n");
1804
+ const parts = lines[1].split(/\s+/);
1805
+ const freeKb = parseInt(parts[3], 10);
1806
+ return freeKb * 1024;
1807
+ }
1808
+ } catch (error) {
1809
+ return null;
1810
+ }
1811
+ }
1812
+
1813
+ // src/utils/fs-utils.ts
1814
+ var import_fs3 = __toESM(require("fs"), 1);
1815
+ var import_path3 = __toESM(require("path"), 1);
1816
+ async function ensurePath(...pathParts) {
1817
+ const fullPath = import_path3.default.resolve(...pathParts);
1818
+ if (!import_fs3.default.existsSync(fullPath)) {
1819
+ await import_fs3.default.promises.mkdir(fullPath, { recursive: true });
1820
+ }
1821
+ return fullPath;
1822
+ }
1823
+ function getFileExtension(dataType) {
1824
+ switch (dataType) {
1825
+ case "json-array":
1826
+ case "json-object":
1827
+ return "json";
1828
+ case "text":
1829
+ return "txt";
1830
+ case "xml":
1831
+ return "xml";
1832
+ default:
1833
+ return "json";
1834
+ }
1835
+ }
1836
+
1837
+ // src/utils/format-utils.ts
1838
+ function bytesToHumanReadable(bytes) {
1839
+ if (bytes === 0) return "0 B";
1840
+ const k = 1024;
1841
+ const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
1842
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1843
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
1844
+ }
1845
+
1846
+ // src/utils/date-utils.ts
1847
+ function isTimestampFolder(folderName) {
1848
+ const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
1849
+ if (!isoRegex.test(folderName)) {
1850
+ return false;
1851
+ }
1852
+ const date = new Date(folderName);
1853
+ return !isNaN(date.getTime()) && date.getTime() > 0;
1854
+ }
1855
+
1856
+ // src/filestore/serializers.ts
1857
+ function detectDataType(data) {
1858
+ if (Array.isArray(data)) {
1859
+ return "json-array";
1860
+ } else if (typeof data === "object" && data !== null) {
1861
+ return "json-object";
1862
+ } else if (typeof data === "string") {
1863
+ const trimmed = data.trim();
1864
+ if (trimmed.startsWith("<?xml") || trimmed.startsWith("<")) {
1865
+ return "xml";
1866
+ }
1867
+ return "text";
1868
+ } else {
1869
+ return "text";
1870
+ }
1871
+ }
1872
+ function serializeData(data) {
1873
+ const dataType = detectDataType(data);
1874
+ if (dataType === "json-array" || dataType === "json-object") {
1875
+ return JSON.stringify(data, null, 4);
1876
+ } else {
1877
+ return String(data);
1878
+ }
1879
+ }
1880
+ function deserializeData(rawData, dataType) {
1881
+ if (dataType === "json-array" || dataType === "json-object") {
1882
+ return JSON.parse(rawData);
1883
+ } else {
1884
+ return rawData;
1885
+ }
1886
+ }
1887
+
1888
+ // src/filestore/synopsis-functions.ts
1889
+ function defaultFileSynopsisFunction(fileEntry, data) {
1890
+ if (!Array.isArray(data) || data.length === 0) {
1891
+ return { ...fileEntry };
1892
+ }
1893
+ const timestamps = [];
1894
+ const statusCounts = {};
1895
+ for (const item of data) {
1896
+ let ts = null;
1897
+ let status = null;
1898
+ for (const [key, value] of Object.entries(item)) {
1899
+ const k = key.toLowerCase();
1900
+ if (k === "modificationtimestamp") {
1901
+ ts = new Date(value).getTime();
1902
+ }
1903
+ if (k === "standardstatus") {
1904
+ status = value;
1905
+ }
1906
+ }
1907
+ if (ts && !isNaN(ts)) {
1908
+ timestamps.push(ts);
1909
+ }
1910
+ if (status !== null && status !== void 0) {
1911
+ statusCounts[status] = (statusCounts[status] || 0) + 1;
1912
+ }
1913
+ }
1914
+ const result = { ...fileEntry };
1915
+ if (timestamps.length) {
1916
+ result.minModificationTimestamp = new Date(Math.min(...timestamps)).toISOString();
1917
+ result.maxModificationTimestamp = new Date(Math.max(...timestamps)).toISOString();
1918
+ }
1919
+ if (Object.keys(statusCounts).length) {
1920
+ result.StandardStatuses = statusCounts;
1921
+ }
1922
+ return result;
1923
+ }
1924
+ function defaultVersionSynopsisFunction(metadata) {
1925
+ if (!metadata?.files || !Array.isArray(metadata.files)) {
1926
+ return metadata;
1927
+ }
1928
+ const timestamps = [];
1929
+ const statusCounts = {};
1930
+ for (const file of metadata.files) {
1931
+ if (file.minModificationTimestamp) {
1932
+ const minTs = new Date(file.minModificationTimestamp).getTime();
1933
+ if (!isNaN(minTs)) timestamps.push(minTs);
1934
+ }
1935
+ if (file.maxModificationTimestamp) {
1936
+ const maxTs = new Date(file.maxModificationTimestamp).getTime();
1937
+ if (!isNaN(maxTs)) timestamps.push(maxTs);
1938
+ }
1939
+ if (file.StandardStatuses && typeof file.StandardStatuses === "object") {
1940
+ for (const [status, count] of Object.entries(file.StandardStatuses)) {
1941
+ statusCounts[status] = (statusCounts[status] || 0) + count;
1942
+ }
1943
+ }
1944
+ }
1945
+ const result = { ...metadata };
1946
+ if (timestamps.length) {
1947
+ result.minModificationTimestamp = new Date(Math.min(...timestamps)).toISOString();
1948
+ result.maxModificationTimestamp = new Date(Math.max(...timestamps)).toISOString();
1949
+ }
1950
+ if (Object.keys(statusCounts).length > 0) {
1951
+ result.StandardStatuses = statusCounts;
1952
+ }
1953
+ return result;
1954
+ }
1955
+
1956
+ // src/filestore/index.ts
1957
+ var FileDatabaseError = class extends Error {
1958
+ constructor(message) {
1959
+ super(message);
1960
+ this.name = "FileDatabaseError";
1961
+ }
1962
+ };
1963
+ var FileDatabase = class {
1964
+ basePath;
1965
+ namespace;
1966
+ tableName = null;
1967
+ maxVersions;
1968
+ pageSize;
1969
+ useMetadata;
1970
+ freeSpaceThreshold;
1971
+ logger;
1972
+ // Current operation state
1973
+ currentVersion = null;
1974
+ currentVersionFolder = null;
1975
+ currentFileNumber = 0;
1976
+ currentRecord = 0;
1977
+ hasReadFirstPage = false;
1978
+ lastFileData = null;
1979
+ metadata;
1980
+ // Synopsis calculation functions
1981
+ fileSynopsisFunction = null;
1982
+ versionSynopsisFunction = null;
1983
+ constructor(config2) {
1984
+ if (!config2.basePath) {
1985
+ throw new ParamError("[FileDatabase] basePath is required");
1986
+ }
1987
+ this.basePath = config2.basePath;
1988
+ this.namespace = config2.namespace || "default";
1989
+ this.tableName = config2.tableName || null;
1990
+ this.maxVersions = config2.maxVersions || 5;
1991
+ this.pageSize = config2.pageSize || 5e3;
1992
+ this.useMetadata = config2.useMetadata !== false;
1993
+ this.freeSpaceThreshold = config2.freeSpaceThreshold || 100 * 1024 * 1024;
1994
+ this.logger = config2.logger || console;
1995
+ this.metadata = this.getDefaultMetadata();
1996
+ }
1997
+ /**
1998
+ * Get default metadata structure
1999
+ */
2000
+ getDefaultMetadata() {
2001
+ return {
2002
+ version: this.currentVersion || null,
2003
+ files: [],
2004
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2005
+ modifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
2006
+ totalRecords: 0,
2007
+ synopsis: null,
2008
+ dataType: null
2009
+ };
2010
+ }
2011
+ /**
2012
+ * Get the destination path (basePath/namespace/tableName)
2013
+ */
2014
+ getDestinationPath() {
2015
+ const errors = ["basePath", "namespace", "tableName"].filter((prop) => !this[prop]).map((prop) => `${prop} is not set`);
2016
+ if (errors.length) {
2017
+ throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
2018
+ }
2019
+ return import_path4.default.resolve(this.basePath, this.namespace, this.tableName);
2020
+ }
2021
+ /**
2022
+ * Set current version and version folder
2023
+ */
2024
+ async setCurrentVersion(version) {
2025
+ this.currentVersion = version;
2026
+ this.currentVersionFolder = await ensurePath(this.getDestinationPath(), version);
2027
+ }
2028
+ /**
2029
+ * Create a new version folder with comprehensive timestamp logic
2030
+ */
2031
+ async makeNewVersion() {
2032
+ const existingVersions = await this.getVersions();
2033
+ let versionName;
2034
+ if (existingVersions.length > 0) {
2035
+ const maxTimestamp = existingVersions.reduce((max, version) => {
2036
+ const versionDate = new Date(version.replace("Z", ""));
2037
+ const maxDate2 = new Date(max.replace("Z", ""));
2038
+ return versionDate > maxDate2 ? version : max;
2039
+ });
2040
+ const maxDate = new Date(maxTimestamp.replace("Z", ""));
2041
+ const nextDate = new Date(maxDate.getTime() + 1e3);
2042
+ versionName = nextDate.toISOString().split(".")[0] + "Z";
2043
+ } else {
2044
+ const now = /* @__PURE__ */ new Date();
2045
+ versionName = now.toISOString().split(".")[0] + "Z";
2046
+ }
2047
+ await this.setCurrentVersion(versionName);
2048
+ this.currentFileNumber = 0;
2049
+ const versions = await this.getVersions();
2050
+ while (versions.length > this.maxVersions) {
2051
+ const versionToDelete = import_path4.default.resolve(this.getDestinationPath(), versions.shift());
2052
+ this.logger.debug?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
2053
+ await import_fs4.default.promises.rm(versionToDelete, { recursive: true, force: true });
2054
+ }
2055
+ return versionName;
2056
+ }
2057
+ /**
2058
+ * Get list of all versions (sorted chronologically)
2059
+ */
2060
+ async getVersions() {
2061
+ const destPath = this.getDestinationPath();
2062
+ try {
2063
+ await ensurePath(destPath);
2064
+ const items = await import_fs4.default.promises.readdir(destPath);
2065
+ const versions = items.filter((item) => {
2066
+ const itemPath = import_path4.default.join(destPath, item);
2067
+ const stat = import_fs4.default.statSync(itemPath);
2068
+ return stat.isDirectory() && isTimestampFolder(item);
2069
+ });
2070
+ return versions.sort();
2071
+ } catch (error) {
2072
+ return [];
2073
+ }
2074
+ }
2075
+ /**
2076
+ * Load metadata from JSON file
2077
+ */
2078
+ async loadMetadataJson(version) {
2079
+ const metadataFile = import_path4.default.join(this.getDestinationPath(), version, "metadata.json");
2080
+ if (import_fs4.default.existsSync(metadataFile)) {
2081
+ try {
2082
+ const rawData = await import_fs4.default.promises.readFile(metadataFile, "utf8");
2083
+ return JSON.parse(rawData);
2084
+ } catch (e) {
2085
+ throw new FileDatabaseError(`Failed to read metadata for version "${version}": ${e.message}`);
2086
+ }
2087
+ }
2088
+ return null;
2089
+ }
2090
+ /**
2091
+ * Build metadata by scanning files in a version folder (backward compatibility)
2092
+ * Reads all files to get accurate counts - used when synopsis calculation is needed
2093
+ */
2094
+ async figureMetadataFromVersionFiles(version) {
2095
+ const versionPath = import_path4.default.join(this.getDestinationPath(), version);
2096
+ if (!import_fs4.default.existsSync(versionPath)) {
2097
+ return this.getDefaultMetadata();
2098
+ }
2099
+ const files = (await import_fs4.default.promises.readdir(versionPath)).filter((file) => file !== "metadata.json" && !file.startsWith(".")).sort();
2100
+ const metadata = this.getDefaultMetadata();
2101
+ metadata.version = version;
2102
+ metadata.files = [];
2103
+ let totalRecords = 0;
2104
+ let detectedDataType = null;
2105
+ for (let i = 0; i < files.length; i++) {
2106
+ const fileName = files[i];
2107
+ const filePath = import_path4.default.join(versionPath, fileName);
2108
+ try {
2109
+ const rawData = await import_fs4.default.promises.readFile(filePath, "utf8");
2110
+ const extension = import_path4.default.extname(fileName).toLowerCase();
2111
+ let dataType = "text";
2112
+ if (extension === ".json") {
2113
+ dataType = "json-array";
2114
+ } else if (extension === ".xml") {
2115
+ dataType = "xml";
2116
+ }
2117
+ const fileData = deserializeData(rawData, dataType);
2118
+ const recordsCount = Array.isArray(fileData) ? fileData.length : 1;
2119
+ if (detectedDataType === null) {
2120
+ detectedDataType = detectDataType(fileData);
2121
+ }
2122
+ const fileInfo = {
2123
+ number: i + 1,
2124
+ recordsCount,
2125
+ fileName
2126
+ };
2127
+ metadata.files.push(fileInfo);
2128
+ totalRecords += recordsCount;
2129
+ } catch (error) {
2130
+ this.logger.error?.(`[FileDatabase] Failed to read file ${fileName}: ${error.message}`);
2131
+ }
2132
+ }
2133
+ metadata.totalRecords = totalRecords;
2134
+ metadata.dataType = detectedDataType;
2135
+ return metadata;
2136
+ }
2137
+ /**
2138
+ * Build metadata optimized - only reads first and last files
2139
+ * Assumes all middle files have the same record count as the first file
2140
+ * Much faster for large datasets with many files
2141
+ */
2142
+ async buildMetadataOptimized(version) {
2143
+ const versionPath = import_path4.default.join(this.getDestinationPath(), version);
2144
+ if (!import_fs4.default.existsSync(versionPath)) {
2145
+ return this.getDefaultMetadata();
2146
+ }
2147
+ const files = (await import_fs4.default.promises.readdir(versionPath)).filter((file) => file !== "metadata.json" && !file.startsWith(".")).sort();
2148
+ if (files.length === 0) {
2149
+ return this.getDefaultMetadata();
2150
+ }
2151
+ const metadata = this.getDefaultMetadata();
2152
+ metadata.version = version;
2153
+ metadata.files = files.map((fileName, index) => ({
2154
+ number: index + 1,
2155
+ recordsCount: 0,
2156
+ fileName
2157
+ }));
2158
+ const firstFile = metadata.files[0];
2159
+ const firstFilePath = import_path4.default.join(versionPath, firstFile.fileName);
2160
+ const firstFileRaw = await import_fs4.default.promises.readFile(firstFilePath, "utf8");
2161
+ let firstFileData;
2162
+ try {
2163
+ firstFileData = JSON.parse(firstFileRaw);
2164
+ } catch (e) {
2165
+ firstFileData = firstFileRaw;
2166
+ }
2167
+ metadata.dataType = detectDataType(firstFileData);
2168
+ if (metadata.dataType === "json-array") {
2169
+ const firstFileCount = Array.isArray(firstFileData) ? firstFileData.length : 1;
2170
+ firstFile.recordsCount = firstFileCount;
2171
+ for (let i = 1; i < metadata.files.length - 1; i++) {
2172
+ metadata.files[i].recordsCount = firstFileCount;
2173
+ }
2174
+ if (files.length > 1) {
2175
+ const lastFile = metadata.files[metadata.files.length - 1];
2176
+ const lastFilePath = import_path4.default.join(versionPath, lastFile.fileName);
2177
+ const lastFileRaw = await import_fs4.default.promises.readFile(lastFilePath, "utf8");
2178
+ const lastFileData = deserializeData(lastFileRaw, metadata.dataType);
2179
+ lastFile.recordsCount = Array.isArray(lastFileData) ? lastFileData.length : 1;
2180
+ }
2181
+ metadata.totalRecords = metadata.files.reduce((sum, file) => sum + file.recordsCount, 0);
2182
+ } else {
2183
+ metadata.files.forEach((file) => {
2184
+ file.recordsCount = 1;
2185
+ });
2186
+ metadata.totalRecords = files.length;
2187
+ }
2188
+ return metadata;
2189
+ }
2190
+ /**
2191
+ * Figure out metadata - tries JSON first, then builds from files
2192
+ * Uses optimized building when no synopsis calculation is needed
2193
+ */
2194
+ async figureMetadata(version, useOptimized = true) {
2195
+ if (this.useMetadata) {
2196
+ const metadata = await this.loadMetadataJson(version);
2197
+ if (metadata) {
2198
+ return metadata;
2199
+ }
2200
+ }
2201
+ if (useOptimized && !this.fileSynopsisFunction && !this.versionSynopsisFunction) {
2202
+ return await this.buildMetadataOptimized(version);
2203
+ }
2204
+ return await this.figureMetadataFromVersionFiles(version);
2205
+ }
2206
+ /**
2207
+ * Load version metadata (main entry point for loading)
2208
+ */
2209
+ async loadVersionMetadata(version) {
2210
+ const metadata = await this.figureMetadata(version);
2211
+ this.metadata = metadata;
2212
+ return metadata;
2213
+ }
2214
+ /**
2215
+ * Save version metadata to file
2216
+ */
2217
+ async saveVersionMetadata(metadata) {
2218
+ if (!this.useMetadata || !this.currentVersion) {
2219
+ return;
2220
+ }
2221
+ const metadataToSave = metadata || this.metadata;
2222
+ const metadataFile = import_path4.default.join(this.getDestinationPath(), this.currentVersion, "metadata.json");
2223
+ await import_fs4.default.promises.writeFile(metadataFile, JSON.stringify(metadataToSave, null, 4), "utf8");
2224
+ }
2225
+ /**
2226
+ * Create a new file entry in metadata
2227
+ */
2228
+ makeNewFile() {
2229
+ this.currentFileNumber = (this.currentFileNumber || 0) + 1;
2230
+ const dataType = this.metadata.dataType || "json-array";
2231
+ const fileEntry = {
2232
+ number: this.currentFileNumber,
2233
+ recordsCount: 0,
2234
+ fileName: `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(dataType)}`
2235
+ };
2236
+ this.metadata.files.push(fileEntry);
2237
+ this.lastFileData = null;
2238
+ this.logger.debug?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
2239
+ }
2240
+ /**
2241
+ * Figure out what data to write and which file to use (for pagination)
2242
+ */
2243
+ figureOutDataAndFileToWrite(data) {
2244
+ let dataToWrite;
2245
+ let dataLeftOver;
2246
+ const lastFile = this.metadata.files[this.metadata.files.length - 1];
2247
+ const lastFileRecordsCount = lastFile.recordsCount;
2248
+ if (Array.isArray(data)) {
2249
+ if (lastFileRecordsCount < this.pageSize) {
2250
+ dataToWrite = [...this.lastFileData || [], ...data.slice(0, this.pageSize - lastFileRecordsCount)];
2251
+ dataLeftOver = data.slice(this.pageSize - lastFileRecordsCount);
2252
+ } else {
2253
+ this.makeNewFile();
2254
+ dataToWrite = data.slice(0, this.pageSize);
2255
+ dataLeftOver = data.slice(this.pageSize);
2256
+ }
2257
+ this.lastFileData = dataToWrite;
2258
+ } else {
2259
+ dataToWrite = data;
2260
+ dataLeftOver = null;
2261
+ }
2262
+ const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
2263
+ this.logger.debug?.(
2264
+ `[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
2265
+ );
2266
+ return { dataToWrite, dataLeftOver, fileName };
2267
+ }
2268
+ /**
2269
+ * Calculate file-level synopsis if function is set
2270
+ */
2271
+ calculateFileSynopsis(data, fileIndex = this.metadata.files.length - 1) {
2272
+ if (!this.fileSynopsisFunction) {
2273
+ return;
2274
+ }
2275
+ const fileInfo = this.metadata.files[fileIndex];
2276
+ const enhancedFileInfo = this.fileSynopsisFunction(fileInfo, data);
2277
+ this.metadata.files[fileIndex] = enhancedFileInfo;
2278
+ }
2279
+ /**
2280
+ * Calculate version-level synopsis if function is set
2281
+ */
2282
+ calculateVersionSynopsis() {
2283
+ if (!this.versionSynopsisFunction) {
2284
+ return;
2285
+ }
2286
+ const enhancedMetadata = this.versionSynopsisFunction(this.metadata);
2287
+ this.metadata = enhancedMetadata;
2288
+ }
2289
+ /**
2290
+ * Update metadata after writing data
2291
+ */
2292
+ updateMetadata(dataToWrite, fileName) {
2293
+ let currentFile;
2294
+ if (fileName) {
2295
+ const foundFile = this.metadata.files.find((file) => file.fileName === fileName);
2296
+ if (!foundFile) {
2297
+ this.logger.warn?.(`[FileDatabase] File ${fileName} not found in metadata, using last file`);
2298
+ currentFile = this.metadata.files[this.metadata.files.length - 1];
2299
+ } else {
2300
+ currentFile = foundFile;
2301
+ }
2302
+ } else {
2303
+ currentFile = this.metadata.files[this.metadata.files.length - 1];
2304
+ }
2305
+ const recordsCount = Array.isArray(dataToWrite) ? dataToWrite.length : 1;
2306
+ currentFile.recordsCount = recordsCount;
2307
+ const fileIndex = this.metadata.files.indexOf(currentFile);
2308
+ if (fileIndex !== -1) {
2309
+ this.calculateFileSynopsis(dataToWrite, fileIndex);
2310
+ }
2311
+ this.metadata.version = this.currentVersion;
2312
+ this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
2313
+ this.metadata.dataType = detectDataType(dataToWrite);
2314
+ this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
2315
+ this.logger.debug?.(
2316
+ `[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
2317
+ );
2318
+ }
2319
+ /**
2320
+ * Safe write with disk space check
2321
+ */
2322
+ async safeWrite(filePath, data) {
2323
+ const serializedData = serializeData(data);
2324
+ const dir = import_path4.default.dirname(filePath);
2325
+ const requiredBytes = Buffer.byteLength(serializedData, "utf8");
2326
+ const freeBytes = getFreeDiskSpace(dir);
2327
+ if (freeBytes !== null) {
2328
+ if (freeBytes < requiredBytes) {
2329
+ throw new FileDatabaseError(
2330
+ `Not enough disk space. Required: ${bytesToHumanReadable(requiredBytes)}, Free: ${bytesToHumanReadable(freeBytes)}`
2331
+ );
2332
+ }
2333
+ if (freeBytes < this.freeSpaceThreshold) {
2334
+ this.logger.warn?.(`Low disk space warning: only ${bytesToHumanReadable(freeBytes)} left`);
2335
+ }
2336
+ }
2337
+ try {
2338
+ await import_fs4.default.promises.writeFile(filePath, serializedData, "utf8");
2339
+ this.logger.debug?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
2340
+ } catch (error) {
2341
+ throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
2342
+ }
2343
+ }
2344
+ /**
2345
+ * Prepare the instance for read or write operations
2346
+ * This discovers state and sets up internal members based on mode and current data
2347
+ */
2348
+ async prepare({ write, read, version }) {
2349
+ if (write) {
2350
+ if (this.currentVersion === null) {
2351
+ await this.makeNewVersion();
2352
+ this.metadata = this.getDefaultMetadata();
2353
+ this.metadata.version = this.currentVersion;
2354
+ this.makeNewFile();
2355
+ } else {
2356
+ if (!this.metadata.files.length) {
2357
+ this.metadata = await this.figureMetadata(this.currentVersion);
2358
+ }
2359
+ }
2360
+ } else if (read) {
2361
+ const versions = await this.getVersions();
2362
+ if (versions.length === 0) {
2363
+ throw new FileDatabaseError("[FileDatabase] No versions found, cannot read");
2364
+ }
2365
+ if (version) {
2366
+ if (!versions.includes(version)) {
2367
+ throw new FileDatabaseError(`[FileDatabase] Version "${version}" not found`);
2368
+ }
2369
+ await this.setCurrentVersion(version);
2370
+ } else {
2371
+ await this.setCurrentVersion(versions[versions.length - 1]);
2372
+ }
2373
+ if (!this.metadata.files.length) {
2374
+ this.metadata = await this.figureMetadata(this.currentVersion);
2375
+ }
2376
+ }
2377
+ }
2378
+ /**
2379
+ * Write data to the file database
2380
+ */
2381
+ async write(data, options = {}) {
2382
+ await this.prepare({ write: true });
2383
+ if (options.forceNewVersion) {
2384
+ await this.makeNewVersion();
2385
+ this.metadata = this.getDefaultMetadata();
2386
+ this.metadata.version = this.currentVersion;
2387
+ this.makeNewFile();
2388
+ }
2389
+ let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data);
2390
+ await this.safeWrite(import_path4.default.join(this.currentVersionFolder, fileName), dataToWrite);
2391
+ this.updateMetadata(dataToWrite, fileName);
2392
+ while (dataLeftOver && dataLeftOver.length > 0) {
2393
+ const writeContext = this.figureOutDataAndFileToWrite(dataLeftOver);
2394
+ await this.safeWrite(import_path4.default.join(this.currentVersionFolder, writeContext.fileName), writeContext.dataToWrite);
2395
+ this.updateMetadata(writeContext.dataToWrite, writeContext.fileName);
2396
+ dataLeftOver = writeContext.dataLeftOver;
2397
+ }
2398
+ this.calculateVersionSynopsis();
2399
+ if (this.useMetadata) {
2400
+ await this.saveVersionMetadata(this.metadata);
2401
+ }
2402
+ }
2403
+ /**
2404
+ * Read data from the file database
2405
+ */
2406
+ async read(options = {}) {
2407
+ const { version, nextPage = false, pageSize } = options;
2408
+ await this.prepare({ read: true, version });
2409
+ const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
2410
+ if (isNonPaginatedData) {
2411
+ const file = this.metadata.files[0];
2412
+ const filePath = import_path4.default.join(this.getDestinationPath(), this.currentVersion, file.fileName);
2413
+ try {
2414
+ const rawData = await import_fs4.default.promises.readFile(filePath, "utf8");
2415
+ return deserializeData(rawData, this.metadata.dataType);
2416
+ } catch (error) {
2417
+ throw new FileDatabaseError(`Failed to read file ${file.fileName}: ${error.message}`);
2418
+ }
2419
+ }
2420
+ let effectivePageSize;
2421
+ if (nextPage && this.hasReadFirstPage) {
2422
+ effectivePageSize = pageSize || this.pageSize;
2423
+ this.currentRecord += effectivePageSize;
2424
+ } else if (!nextPage) {
2425
+ effectivePageSize = pageSize !== void 0 ? pageSize : this.metadata.totalRecords;
2426
+ this.currentRecord = 0;
2427
+ } else {
2428
+ effectivePageSize = pageSize || this.pageSize;
2429
+ }
2430
+ if (this.currentRecord >= this.metadata.totalRecords) {
2431
+ return [];
2432
+ }
2433
+ const result = [];
2434
+ let recordsRead = 0;
2435
+ let currentFileIndex = 0;
2436
+ let currentFileOffset = 0;
2437
+ let totalRecords = 0;
2438
+ for (let i = 0; i < this.metadata.files.length; i++) {
2439
+ const file = this.metadata.files[i];
2440
+ if (this.currentRecord < totalRecords + file.recordsCount) {
2441
+ currentFileIndex = i;
2442
+ currentFileOffset = totalRecords;
2443
+ break;
2444
+ }
2445
+ totalRecords += file.recordsCount;
2446
+ }
2447
+ let cumulativeRecords = currentFileOffset;
2448
+ for (let i = currentFileIndex; i < this.metadata.files.length && recordsRead < effectivePageSize; i++) {
2449
+ const file = this.metadata.files[i];
2450
+ const filePath = import_path4.default.join(this.getDestinationPath(), this.currentVersion, file.fileName);
2451
+ try {
2452
+ const rawData = await import_fs4.default.promises.readFile(filePath, "utf8");
2453
+ const fileData = deserializeData(rawData, this.metadata.dataType);
2454
+ let startIndex = 0;
2455
+ if (i === currentFileIndex) {
2456
+ startIndex = this.currentRecord - cumulativeRecords;
2457
+ }
2458
+ const endIndex = Math.min(startIndex + (effectivePageSize - recordsRead), fileData.length);
2459
+ const recordsFromThisFile = fileData.slice(startIndex, endIndex);
2460
+ result.push(...recordsFromThisFile);
2461
+ recordsRead += recordsFromThisFile.length;
2462
+ cumulativeRecords += file.recordsCount;
2463
+ } catch (error) {
2464
+ throw new FileDatabaseError(`Failed to read file ${file.fileName}: ${error.message}`);
2465
+ }
2466
+ }
2467
+ if (result.length > 0) {
2468
+ if (nextPage || pageSize !== void 0 && pageSize < this.metadata.totalRecords) {
2469
+ this.hasReadFirstPage = true;
2470
+ }
2471
+ }
2472
+ return result;
2473
+ }
2474
+ /**
2475
+ * Set the starting record for pagination (1-based index)
2476
+ */
2477
+ setStartRecord(startRecord) {
2478
+ this.currentRecord = startRecord - 1;
2479
+ this.hasReadFirstPage = false;
2480
+ }
2481
+ /**
2482
+ * Reset read pagination state
2483
+ */
2484
+ resetPagination() {
2485
+ this.currentRecord = 0;
2486
+ this.hasReadFirstPage = false;
2487
+ }
2488
+ /**
2489
+ * Set file-level synopsis calculation function
2490
+ */
2491
+ setFileSynopsisFunction(fn) {
2492
+ this.fileSynopsisFunction = fn;
2493
+ }
2494
+ /**
2495
+ * Set version-level synopsis calculation function
2496
+ */
2497
+ setVersionSynopsisFunction(fn) {
2498
+ this.versionSynopsisFunction = fn;
2499
+ }
2500
+ /**
2501
+ * Get current version name
2502
+ */
2503
+ getCurrentVersion() {
2504
+ return this.currentVersion;
2505
+ }
2506
+ /**
2507
+ * Get current metadata
2508
+ */
2509
+ getMetadata() {
2510
+ return { ...this.metadata };
2511
+ }
2512
+ };
1777
2513
  // Annotate the CommonJS export names for ESM import in node:
1778
2514
  0 && (module.exports = {
1779
2515
  Args,
1780
2516
  Divider,
2517
+ FileDatabase,
2518
+ FileDatabaseError,
1781
2519
  FooterPresets,
1782
2520
  GridCell,
1783
2521
  InputField,
@@ -1796,6 +2534,8 @@ function organizeFooterMessages(messages) {
1796
2534
  buildBreadcrumb,
1797
2535
  buildDetailBreadcrumb,
1798
2536
  buildFooter,
2537
+ defaultFileSynopsisFunction,
2538
+ defaultVersionSynopsisFunction,
1799
2539
  getArgsInstance,
1800
2540
  getParamsInstance,
1801
2541
  joiEdateType,