inibase 1.0.0-rc.101 → 1.0.0-rc.103

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/file.d.ts CHANGED
@@ -58,7 +58,7 @@ export declare function get(filePath: string, lineNumbers: undefined | number |
58
58
  *
59
59
  * Note: If the file doesn't exist and replacements is an object, it creates a new file with the specified replacements.
60
60
  */
61
- export declare const replace: (filePath: string, replacements: string | number | boolean | null | (string | number | boolean | null)[] | Record<number, string | boolean | number | null | (string | boolean | number | null)[]>) => Promise<string[]>;
61
+ export declare const replace: (filePath: string, replacements: string | number | boolean | null | (string | number | boolean | null)[] | Record<number, string | boolean | number | null | (string | boolean | number | null)[]>, totalItems?: number) => Promise<string[]>;
62
62
  /**
63
63
  * Asynchronously appends data to the end of a file.
64
64
  *
package/dist/file.js CHANGED
@@ -1,9 +1,11 @@
1
+ import { createWriteStream } from "node:fs";
1
2
  import { access, appendFile, copyFile, constants as fsConstants, open, readFile, unlink, writeFile, } from "node:fs/promises";
2
3
  import { join } from "node:path";
3
4
  import { createInterface } from "node:readline";
4
5
  import { Transform } from "node:stream";
5
6
  import { pipeline } from "node:stream/promises";
6
7
  import { createGunzip, createGzip } from "node:zlib";
8
+ import { spawn as spawnSync } from "node:child_process";
7
9
  import Inison from "inison";
8
10
  import { detectFieldType, isArrayOfObjects, isStringified, isNumber, isObject, } from "./utils.js";
9
11
  import { compare, encodeID, exec, gunzip, gzip } from "./utils.server.js";
@@ -34,6 +36,10 @@ export const write = async (filePath, data) => {
34
36
  export const read = async (filePath) => filePath.endsWith(".gz")
35
37
  ? (await gunzip(await readFile(filePath, "utf8"))).toString()
36
38
  : await readFile(filePath, "utf8");
39
+ // Escaping function to safely handle special characters
40
+ function _escapeDoubleQuotes(arg) {
41
+ return arg.replace(/(["$`\\])/g, "\\$1"); // Escape double quotes, $, `, and \
42
+ }
37
43
  const _pipeline = async (filePath, rl, writeStream, transform) => {
38
44
  if (filePath.endsWith(".gz"))
39
45
  await pipeline(rl, transform, createGzip(), writeStream);
@@ -192,9 +198,10 @@ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, s
192
198
  }
193
199
  }
194
200
  else if (lineNumbers == -1) {
201
+ const escapedFilePath = `"${_escapeDoubleQuotes(filePath)}"`;
195
202
  const command = filePath.endsWith(".gz")
196
- ? `zcat ${filePath} | sed -n '$p'`
197
- : `sed -n '$p' ${filePath}`, foundedLine = (await exec(command)).stdout.trimEnd();
203
+ ? `zcat ${escapedFilePath} | sed -n '$p'`
204
+ : `sed -n '$p' ${escapedFilePath}`, foundedLine = (await exec(command)).stdout.trimEnd();
198
205
  if (foundedLine)
199
206
  lines[linesCount] = decode(foundedLine, fieldType, fieldChildrenType, secretKey);
200
207
  }
@@ -213,9 +220,10 @@ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, s
213
220
  }
214
221
  return [lines, linesCount];
215
222
  }
223
+ const escapedFilePath = `"${_escapeDoubleQuotes(filePath)}"`;
216
224
  const command = filePath.endsWith(".gz")
217
- ? `zcat ${filePath} | sed -n '${lineNumbers.join("p;")}p'`
218
- : `sed -n '${lineNumbers.join("p;")}p' ${filePath}`, foundedLines = (await exec(command)).stdout.trimEnd().split("\n");
225
+ ? `zcat "${escapedFilePath}" | sed -n '${lineNumbers.join("p;")}p'`
226
+ : `sed -n '${lineNumbers.join("p;")}p' "${escapedFilePath}"`, foundedLines = (await exec(command)).stdout.trimEnd().split("\n");
219
227
  let index = 0;
220
228
  for (const line of foundedLines) {
221
229
  lines[lineNumbers[index]] = decode(line, fieldType, fieldChildrenType, secretKey);
@@ -239,48 +247,93 @@ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, s
239
247
  *
240
248
  * Note: If the file doesn't exist and replacements is an object, it creates a new file with the specified replacements.
241
249
  */
242
- export const replace = async (filePath, replacements) => {
250
+ export const replace = async (filePath, replacements, totalItems) => {
243
251
  const fileTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/$1");
252
+ const isReplacementsObject = isObject(replacements);
253
+ const isReplacementsObjectHasLineNumbers = isReplacementsObject && !Number.isNaN(Number(Object.keys(replacements)[0]));
244
254
  if (await isExists(filePath)) {
245
- let fileHandle = null;
246
- let fileTempHandle = null;
255
+ if (isReplacementsObjectHasLineNumbers) {
256
+ let fileHandle = null;
257
+ let fileTempHandle = null;
258
+ try {
259
+ let linesCount = 0;
260
+ fileHandle = await open(filePath, "r");
261
+ fileTempHandle = await open(fileTempPath, "w");
262
+ const rl = createReadLineInternface(filePath, fileHandle);
263
+ await _pipeline(filePath, rl, fileTempHandle.createWriteStream(), new Transform({
264
+ transform(line, _, callback) {
265
+ linesCount++;
266
+ const replacement = isReplacementsObject
267
+ ? Object.hasOwn(replacements, linesCount)
268
+ ? replacements[linesCount]
269
+ : line
270
+ : replacements;
271
+ return callback(null, `${replacement}\n`);
272
+ },
273
+ }));
274
+ return [fileTempPath, filePath];
275
+ }
276
+ catch {
277
+ return [fileTempPath, null];
278
+ }
279
+ finally {
280
+ // Ensure that file handles are closed, even if an error occurred
281
+ await fileHandle?.close();
282
+ await fileTempHandle?.close();
283
+ }
284
+ }
285
+ else {
286
+ return new Promise((resolve) => {
287
+ const sedProcess = spawnSync("sed", [
288
+ "-e",
289
+ `s/.*/${replacements}/`,
290
+ "-e",
291
+ `/^$/s/^/${replacements}/`,
292
+ filePath,
293
+ ]);
294
+ const outputStream = createWriteStream(fileTempPath); // Temp file for output
295
+ // Pipe sed output to the temporary file
296
+ sedProcess.stdout.pipe(outputStream);
297
+ // Handle the process close
298
+ sedProcess.on("close", (code) => {
299
+ if (code === 0)
300
+ resolve([fileTempPath, filePath]);
301
+ else
302
+ resolve([fileTempPath, null]);
303
+ });
304
+ // Handle errors in spawning the sed process
305
+ sedProcess.on("error", () => {
306
+ resolve([fileTempPath, null]);
307
+ });
308
+ });
309
+ }
310
+ }
311
+ else if (isReplacementsObject) {
247
312
  try {
248
- let linesCount = 0;
249
- fileHandle = await open(filePath, "r");
250
- fileTempHandle = await open(fileTempPath, "w");
251
- const rl = createReadLineInternface(filePath, fileHandle);
252
- await _pipeline(filePath, rl, fileTempHandle.createWriteStream(), new Transform({
253
- transform(line, _, callback) {
254
- linesCount++;
255
- const replacement = isObject(replacements)
256
- ? Object.hasOwn(replacements, linesCount)
257
- ? replacements[linesCount]
258
- : line
259
- : replacements;
260
- return callback(null, `${replacement}\n`);
261
- },
262
- }));
313
+ if (isReplacementsObjectHasLineNumbers) {
314
+ const replacementsKeys = Object.keys(replacements)
315
+ .map(Number)
316
+ .toSorted((a, b) => a - b);
317
+ await write(fileTempPath, `${"\n".repeat(replacementsKeys[0] - 1) +
318
+ replacementsKeys
319
+ .map((lineNumber, index) => index === 0 ||
320
+ lineNumber - replacementsKeys[index - 1] - 1 === 0
321
+ ? replacements[lineNumber]
322
+ : "\n".repeat(lineNumber - replacementsKeys[index - 1] - 1) +
323
+ replacements[lineNumber])
324
+ .join("\n")}\n`);
325
+ }
326
+ else {
327
+ if (!totalItems)
328
+ throw new Error("INVALID_PARAMETERS");
329
+ await write(fileTempPath, `${`${replacements}\n`.repeat(totalItems)}\n`);
330
+ }
263
331
  return [fileTempPath, filePath];
264
332
  }
265
- finally {
266
- // Ensure that file handles are closed, even if an error occurred
267
- await fileHandle?.close();
268
- await fileTempHandle?.close();
333
+ catch {
334
+ return [fileTempPath, null];
269
335
  }
270
336
  }
271
- else if (isObject(replacements)) {
272
- const replacementsKeys = Object.keys(replacements)
273
- .map(Number)
274
- .toSorted((a, b) => a - b);
275
- await write(fileTempPath, `${"\n".repeat(replacementsKeys[0] - 1) +
276
- replacementsKeys
277
- .map((lineNumber, index) => index === 0 || lineNumber - replacementsKeys[index - 1] - 1 === 0
278
- ? replacements[lineNumber]
279
- : "\n".repeat(lineNumber - replacementsKeys[index - 1] - 1) +
280
- replacements[lineNumber])
281
- .join("\n")}\n`);
282
- return [fileTempPath, filePath];
283
- }
284
337
  return [];
285
338
  };
286
339
  /**
@@ -293,20 +346,26 @@ export const replace = async (filePath, replacements) => {
293
346
  */
294
347
  export const append = async (filePath, data) => {
295
348
  const fileTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/$1");
296
- if (await isExists(filePath)) {
297
- await copyFile(filePath, fileTempPath);
298
- if (!filePath.endsWith(".gz")) {
299
- await appendFile(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
300
- }
301
- else {
302
- await exec(`echo $'${(Array.isArray(data) ? data.join("\n") : data)
303
- .toString()
304
- .replace(/'/g, "\\'")}' | gzip - >> ${fileTempPath}`);
349
+ try {
350
+ if (await isExists(filePath)) {
351
+ await copyFile(filePath, fileTempPath);
352
+ if (!filePath.endsWith(".gz")) {
353
+ await appendFile(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
354
+ }
355
+ else {
356
+ const escapedFileTempPath = `"${_escapeDoubleQuotes(fileTempPath)}"`;
357
+ await exec(`echo $'${(Array.isArray(data) ? data.join("\n") : data)
358
+ .toString()
359
+ .replace(/'/g, "\\'")}' | gzip - >> ${escapedFileTempPath}`);
360
+ }
305
361
  }
362
+ else
363
+ await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
364
+ return [fileTempPath, filePath];
365
+ }
366
+ catch {
367
+ return [fileTempPath, null];
306
368
  }
307
- else
308
- await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
309
- return [fileTempPath, filePath];
310
369
  };
311
370
  /**
312
371
  * Asynchronously prepends data to the beginning of a file.
@@ -337,6 +396,9 @@ export const prepend = async (filePath, data) => {
337
396
  },
338
397
  }));
339
398
  }
399
+ catch {
400
+ return [fileTempPath, null];
401
+ }
340
402
  finally {
341
403
  // Ensure that file handles are closed, even if an error occurred
342
404
  await fileHandle?.close();
@@ -347,15 +409,27 @@ export const prepend = async (filePath, data) => {
347
409
  const fileChildTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/tmp_$1");
348
410
  try {
349
411
  await write(fileChildTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
350
- await exec(`cat ${fileChildTempPath} ${filePath} > ${fileTempPath}`);
412
+ const escapedFilePath = `"${_escapeDoubleQuotes(filePath)}"`;
413
+ const escapedFileTempPath = `"${_escapeDoubleQuotes(fileTempPath)}"`;
414
+ const escapedFileChildTempPath = `"${_escapeDoubleQuotes(fileChildTempPath)}"`;
415
+ await exec(`cat ${escapedFileChildTempPath} ${escapedFilePath} > ${escapedFileTempPath}`);
416
+ }
417
+ catch {
418
+ return [fileTempPath, null];
351
419
  }
352
420
  finally {
353
421
  await unlink(fileChildTempPath);
354
422
  }
355
423
  }
356
424
  }
357
- else
358
- await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
425
+ else {
426
+ try {
427
+ await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
428
+ }
429
+ catch {
430
+ return [fileTempPath, null];
431
+ }
432
+ }
359
433
  return [fileTempPath, filePath];
360
434
  };
361
435
  /**
@@ -374,11 +448,18 @@ export const remove = async (filePath, linesToDelete) => {
374
448
  if (linesToDelete.some(Number.isNaN))
375
449
  throw new Error("UNVALID_LINE_NUMBERS");
376
450
  const fileTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/$1");
377
- const command = filePath.endsWith(".gz")
378
- ? `zcat ${filePath} | sed "${linesToDelete.join("d;")}d" | gzip > ${fileTempPath}`
379
- : `sed "${linesToDelete.join("d;")}d" ${filePath} > ${fileTempPath}`;
380
- await exec(command);
381
- return [fileTempPath, filePath];
451
+ try {
452
+ const escapedFilePath = `"${_escapeDoubleQuotes(filePath)}"`;
453
+ const escapedFileTempPath = `"${_escapeDoubleQuotes(fileTempPath)}"`;
454
+ const command = filePath.endsWith(".gz")
455
+ ? `zcat ${escapedFilePath} | sed "${linesToDelete.join("d;")}d" | gzip > ${fileTempPath}`
456
+ : `sed "${linesToDelete.join("d;")}d" ${escapedFilePath} > ${escapedFileTempPath}`;
457
+ await exec(command);
458
+ return [fileTempPath, filePath];
459
+ }
460
+ catch {
461
+ return [fileTempPath, null];
462
+ }
382
463
  };
383
464
  /**
384
465
  * Asynchronously searches a file for lines matching specified criteria, using comparison and logical operators.
package/dist/index.d.ts CHANGED
@@ -49,7 +49,7 @@ declare global {
49
49
  entries<T extends object>(o: T): Entries<T>;
50
50
  }
51
51
  }
52
- export type ErrorCodes = "FIELD_UNIQUE" | "FIELD_REQUIRED" | "NO_SCHEMA" | "NO_ITEMS" | "INVALID_ID" | "INVALID_TYPE" | "INVALID_PARAMETERS" | "NO_ENV" | "TABLE_EXISTS" | "TABLE_NOT_EXISTS";
52
+ export type ErrorCodes = "FIELD_UNIQUE" | "FIELD_REQUIRED" | "NO_SCHEMA" | "TABLE_EMPTY" | "INVALID_ID" | "INVALID_TYPE" | "INVALID_PARAMETERS" | "NO_ENV" | "TABLE_EXISTS" | "TABLE_NOT_EXISTS";
53
53
  export type ErrorLang = "en";
54
54
  export default class Inibase {
55
55
  pageInfo: Record<string, pageInfo>;
@@ -72,7 +72,6 @@ export default class Inibase {
72
72
  */
73
73
  createTable(tableName: string, schema?: Schema, config?: Config): Promise<void>;
74
74
  private replaceStringInFile;
75
- private replaceStringInSchemas;
76
75
  /**
77
76
  * Update table schema or config
78
77
  *
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import "dotenv/config";
2
2
  import { randomBytes, scryptSync } from "node:crypto";
3
3
  import { appendFileSync, existsSync, readFileSync } from "node:fs";
4
- import { mkdir, readFile, readdir, rename, stat, unlink, writeFile, } from "node:fs/promises";
4
+ import { glob, mkdir, readFile, readdir, rename, rm, unlink, writeFile, } from "node:fs/promises";
5
5
  import { join, parse } from "node:path";
6
6
  import { inspect } from "node:util";
7
7
  import Inison from "inison";
8
8
  import * as File from "./file.js";
9
9
  import * as Utils from "./utils.js";
10
10
  import * as UtilsServer from "./utils.server.js";
11
+ // hide ExperimentalWarning glob()
12
+ process.removeAllListeners("warning");
11
13
  export default class Inibase {
12
14
  pageInfo;
13
15
  salt;
@@ -35,12 +37,12 @@ export default class Inibase {
35
37
  Error(code, variable, language = "en") {
36
38
  const errorMessages = {
37
39
  en: {
38
- FIELD_UNIQUE: "Field {variable} should be unique, got {variable} instead",
39
- FIELD_REQUIRED: "Field {variable} is required",
40
+ TABLE_EMPTY: "Table {variable} is empty",
40
41
  TABLE_EXISTS: "Table {variable} already exists",
41
42
  TABLE_NOT_EXISTS: "Table {variable} doesn't exist",
42
43
  NO_SCHEMA: "Table {variable} does't have a schema",
43
- NO_ITEMS: "Table {variable} is empty",
44
+ FIELD_UNIQUE: "Field {variable} should be unique, got {variable} instead",
45
+ FIELD_REQUIRED: "Field {variable} is required",
44
46
  INVALID_ID: "The given ID(s) is/are not valid(s)",
45
47
  INVALID_TYPE: "Expect {variable} to be {variable}, got {variable} instead",
46
48
  INVALID_PARAMETERS: "The given parameters are not valid",
@@ -108,8 +110,14 @@ export default class Inibase {
108
110
  if (config.prepend)
109
111
  await writeFile(join(tablePath, ".prepend.config"), "");
110
112
  }
111
- if (schema)
112
- await writeFile(join(tablePath, "schema.json"), JSON.stringify(UtilsServer.addIdToSchema(schema, 0, this.salt), null, 2));
113
+ if (schema) {
114
+ const lastSchemaId = 0;
115
+ await writeFile(join(tablePath, "schema.json"), JSON.stringify(UtilsServer.addIdToSchema(schema, lastSchemaId, this.salt), null, 2));
116
+ await writeFile(join(tablePath, `${lastSchemaId}.schema`), "");
117
+ }
118
+ else
119
+ await writeFile(join(tablePath, "0.schema"), "");
120
+ await writeFile(join(tablePath, "0-0.pagination"), "");
113
121
  }
114
122
  // Function to replace the string in one schema.json file
115
123
  async replaceStringInFile(filePath, targetString, replaceString) {
@@ -119,20 +127,6 @@ export default class Inibase {
119
127
  await writeFile(filePath, updatedContent, "utf8");
120
128
  }
121
129
  }
122
- // Function to process schema files one by one (sequentially)
123
- async replaceStringInSchemas(directoryPath, targetString, replaceString) {
124
- const files = await readdir(directoryPath);
125
- for (const file of files) {
126
- const fullPath = join(directoryPath, file);
127
- const fileStat = await stat(fullPath);
128
- if (fileStat.isDirectory()) {
129
- await this.replaceStringInSchemas(fullPath, targetString, replaceString);
130
- }
131
- else if (file === "schema.json") {
132
- await this.replaceStringInFile(fullPath, targetString, replaceString);
133
- }
134
- }
135
- }
136
130
  /**
137
131
  * Update table schema or config
138
132
  *
@@ -145,11 +139,13 @@ export default class Inibase {
145
139
  if (schema) {
146
140
  // remove id from schema
147
141
  schema = schema.filter(({ key }) => !["id", "createdAt", "updatedAt"].includes(key));
142
+ let schemaIdFilePath;
143
+ for await (const filePath of glob("*.schema", { cwd: this.databasePath }))
144
+ schemaIdFilePath = filePath;
145
+ const lastSchemaId = Number(parse(schemaIdFilePath).name);
148
146
  if (await File.isExists(join(tablePath, "schema.json"))) {
149
147
  // update columns files names based on field id
150
- schema = UtilsServer.addIdToSchema(schema, table.schema?.length
151
- ? UtilsServer.findLastIdNumber(table.schema, this.salt)
152
- : 0, this.salt);
148
+ schema = UtilsServer.addIdToSchema(schema, lastSchemaId, this.salt);
153
149
  if (table.schema?.length) {
154
150
  const replaceOldPathes = Utils.findChangedProperties(this._schemaToIdsPath(tableName, table.schema), this._schemaToIdsPath(tableName, schema));
155
151
  if (replaceOldPathes)
@@ -160,8 +156,9 @@ export default class Inibase {
160
156
  }
161
157
  }
162
158
  else
163
- schema = UtilsServer.addIdToSchema(schema, 0, this.salt);
159
+ schema = UtilsServer.addIdToSchema(schema, lastSchemaId, this.salt);
164
160
  await writeFile(join(tablePath, "schema.json"), JSON.stringify(schema, null, 2));
161
+ await rename(schemaIdFilePath, join(tablePath, `${lastSchemaId}.schema`));
165
162
  }
166
163
  if (config) {
167
164
  if (config.compression !== undefined &&
@@ -186,8 +183,10 @@ export default class Inibase {
186
183
  if (config.cache !== undefined && config.cache !== table.config.cache) {
187
184
  if (config.cache)
188
185
  await writeFile(join(tablePath, ".cache.config"), "");
189
- else
186
+ else {
187
+ await this.clearCache(tableName);
190
188
  await unlink(join(tablePath, ".cache.config"));
189
+ }
191
190
  }
192
191
  if (config.prepend !== undefined &&
193
192
  config.prepend !== table.config.prepend) {
@@ -213,8 +212,12 @@ export default class Inibase {
213
212
  await unlink(join(tablePath, ".prepend.config"));
214
213
  }
215
214
  if (config.name) {
216
- await this.replaceStringInSchemas(this.databasePath, `"table": "${tableName}"`, `"table": "${config.name}"`);
217
215
  await rename(tablePath, join(this.databasePath, config.name));
216
+ // replace table name in other linked tables (relationship)
217
+ for await (const schemaPath of glob("**/schema.json", {
218
+ cwd: this.databasePath,
219
+ }))
220
+ await this.replaceStringInFile(schemaPath, `"table": "${tableName}"`, `"table": "${config.name}"`);
218
221
  }
219
222
  }
220
223
  delete this.tables[tableName];
@@ -241,14 +244,13 @@ export default class Inibase {
241
244
  return this.tables[tableName];
242
245
  }
243
246
  async getTableSchema(tableName, encodeIDs = true) {
244
- const tableSchemaPath = join(this.databasePath, tableName, "schema.json");
245
- if (!(await File.isExists(tableSchemaPath)))
247
+ const tablePath = join(this.databasePath, tableName);
248
+ if (!(await File.isExists(join(tablePath, "schema.json"))))
246
249
  return undefined;
247
- const schemaFile = await readFile(tableSchemaPath, "utf8");
250
+ const schemaFile = await readFile(join(tablePath, "schema.json"), "utf8");
248
251
  if (!schemaFile)
249
252
  return undefined;
250
253
  let schema = JSON.parse(schemaFile);
251
- const lastIdNumber = UtilsServer.findLastIdNumber(schema, this.salt);
252
254
  schema = [
253
255
  {
254
256
  id: 0,
@@ -256,19 +258,19 @@ export default class Inibase {
256
258
  type: "id",
257
259
  required: true,
258
260
  },
259
- ...schema,
260
261
  {
261
- id: lastIdNumber + 1,
262
+ id: -1,
262
263
  key: "createdAt",
263
264
  type: "date",
264
265
  required: true,
265
266
  },
266
267
  {
267
- id: lastIdNumber + 2,
268
+ id: -2,
268
269
  key: "updatedAt",
269
270
  type: "date",
270
271
  required: false,
271
272
  },
273
+ ...schema,
272
274
  ];
273
275
  if (!encodeIDs)
274
276
  return schema;
@@ -279,7 +281,7 @@ export default class Inibase {
279
281
  if (!table.schema)
280
282
  throw this.Error("NO_SCHEMA", tableName);
281
283
  if (!(await File.isExists(join(this.databasePath, tableName, `id${this.getFileExtension(tableName)}`))))
282
- throw this.Error("NO_ITEMS", tableName);
284
+ throw this.Error("TABLE_EMPTY", tableName);
283
285
  return table;
284
286
  }
285
287
  validateData(data, schema, skipRequiredField = false) {
@@ -455,15 +457,17 @@ export default class Inibase {
455
457
  return null;
456
458
  }
457
459
  }
458
- _combineObjectsToArray = (input) => input.reduce((result, current) => {
459
- for (const [key, value] of Object.entries(current))
460
- if (Object.hasOwn(result, key) && Array.isArray(result[key]))
461
- result[key].push(value);
462
- else
463
- result[key] = [value];
464
- return result;
465
- }, {});
466
- _CombineData = (data, prefix) => {
460
+ _combineObjectsToArray(input) {
461
+ return input.reduce((result, current) => {
462
+ for (const [key, value] of Object.entries(current))
463
+ if (Object.hasOwn(result, key) && Array.isArray(result[key]))
464
+ result[key].push(value);
465
+ else
466
+ result[key] = [value];
467
+ return result;
468
+ }, {});
469
+ }
470
+ _CombineData(data, prefix) {
467
471
  if (Utils.isArrayOfObjects(data))
468
472
  return this._combineObjectsToArray(data.map((single_data) => this._CombineData(single_data)));
469
473
  const RETURN = {};
@@ -480,7 +484,7 @@ export default class Inibase {
480
484
  RETURN[(prefix ?? "") + key] = File.encode(value);
481
485
  }
482
486
  return RETURN;
483
- };
487
+ }
484
488
  joinPathesContents(tableName, data) {
485
489
  const tablePath = join(this.databasePath, tableName), combinedData = this._CombineData(data);
486
490
  const newCombinedData = {};
@@ -834,9 +838,8 @@ export default class Inibase {
834
838
  */
835
839
  async clearCache(tableName) {
836
840
  const cacheFolderPath = join(this.databasePath, tableName, ".cache");
837
- await Promise.all((await readdir(cacheFolderPath))
838
- .filter((file) => file !== ".pagination")
839
- .map((file) => unlink(join(cacheFolderPath, file))));
841
+ await rm(cacheFolderPath, { recursive: true, force: true });
842
+ await mkdir(cacheFolderPath);
840
843
  }
841
844
  async get(tableName, where, options = {
842
845
  page: 1,
@@ -858,7 +861,12 @@ export default class Inibase {
858
861
  let schema = (await this.getTable(tableName)).schema;
859
862
  if (!schema)
860
863
  throw this.Error("NO_SCHEMA", tableName);
861
- if (!(await File.isExists(join(tablePath, `id${this.getFileExtension(tableName)}`))))
864
+ let pagination;
865
+ for await (const paginationFilePath of glob("*.pagination", {
866
+ cwd: tablePath,
867
+ }))
868
+ pagination = parse(paginationFilePath).name.split("-").map(Number);
869
+ if (!pagination[1])
862
870
  return null;
863
871
  if (options.columns?.length)
864
872
  schema = this._filterSchemaByColumns(schema, options.columns);
@@ -931,7 +939,7 @@ export default class Inibase {
931
939
  if (where)
932
940
  lines = lines.slice((options.page - 1) * options.perPage, options.page * options.perPage);
933
941
  else if (!this.totalItems[`${tableName}-*`])
934
- this.totalItems[`${tableName}-*`] = await File.count(join(tablePath, `id${this.getFileExtension(tableName)}`));
942
+ this.totalItems[`${tableName}-*`] = pagination[1];
935
943
  if (!lines.length)
936
944
  return null;
937
945
  // Parse the result and extract the specified lines
@@ -964,16 +972,8 @@ export default class Inibase {
964
972
  RETURN = Object.values(await this.getItemsFromSchema(tableName, schema, Array.from({ length: options.perPage }, (_, index) => (options.page - 1) * options.perPage +
965
973
  index +
966
974
  1), options));
967
- if (await File.isExists(join(tablePath, ".cache", ".pagination"))) {
968
- if (!this.totalItems[`${tableName}-*`])
969
- this.totalItems[`${tableName}-*`] = Number((await readFile(join(tablePath, ".cache", ".pagination"), "utf8")).split(",")[1]);
970
- }
971
- else {
972
- const lastId = Number(Object.keys((await File.get(join(tablePath, `id${this.getFileExtension(tableName)}`), -1, "number", undefined, this.salt, true))?.[0] ?? 0));
973
- if (!this.totalItems[`${tableName}-*`])
974
- this.totalItems[`${tableName}-*`] = await File.count(join(tablePath, `id${this.getFileExtension(tableName)}`));
975
- await writeFile(join(tablePath, ".cache", ".pagination"), `${lastId},${this.totalItems[`${tableName}-*`]}`);
976
- }
975
+ if (!this.totalItems[`${tableName}-*`])
976
+ this.totalItems[`${tableName}-*`] = pagination[1];
977
977
  }
978
978
  else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
979
979
  Utils.isNumber(where)) {
@@ -1076,23 +1076,17 @@ export default class Inibase {
1076
1076
  returnPostedData = false;
1077
1077
  const keys = UtilsServer.hashString(Object.keys(Array.isArray(data) ? data[0] : data).join("."));
1078
1078
  // Skip Id and (created|updated)At
1079
- this.validateData(data, schema.slice(1, -2));
1080
- let lastId = 0, renameList = [];
1079
+ this.validateData(data, schema.slice(3));
1080
+ let lastId = 0;
1081
+ const renameList = [];
1081
1082
  try {
1082
1083
  await File.lock(join(tablePath, ".tmp"), keys);
1083
- if (await File.isExists(join(tablePath, `id${this.getFileExtension(tableName)}`))) {
1084
- if (await File.isExists(join(tablePath, ".cache", ".pagination")))
1085
- [lastId, this.totalItems[`${tableName}-*`]] = (await readFile(join(tablePath, ".cache", ".pagination"), "utf8"))
1086
- .split(",")
1087
- .map(Number);
1088
- else {
1089
- lastId = Number(Object.keys((await File.get(join(tablePath, `id${this.getFileExtension(tableName)}`), -1, "number", undefined, this.salt, true))?.[0] ?? 0));
1090
- if (!this.totalItems[`${tableName}-*`])
1091
- this.totalItems[`${tableName}-*`] = await File.count(join(tablePath, `id${this.getFileExtension(tableName)}`));
1092
- }
1093
- }
1094
- else
1095
- this.totalItems[`${tableName}-*`] = 0;
1084
+ let paginationFilePath;
1085
+ for await (const filePath of glob("*.pagination", { cwd: tablePath }))
1086
+ paginationFilePath = filePath;
1087
+ [lastId, this.totalItems[`${tableName}-*`]] = parse(paginationFilePath)
1088
+ .name.split("-")
1089
+ .map(Number);
1096
1090
  if (Utils.isArrayOfObjects(data))
1097
1091
  for (let index = 0; index < data.length; index++) {
1098
1092
  const element = data[index];
@@ -1112,17 +1106,18 @@ export default class Inibase {
1112
1106
  ? data.toReversed()
1113
1107
  : data
1114
1108
  : data);
1115
- await Promise.all(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(this.tables[tableName].config.prepend
1109
+ await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(this.tables[tableName].config.prepend
1116
1110
  ? await File.prepend(path, content)
1117
1111
  : await File.append(path, content))));
1118
- await Promise.all(renameList.map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1119
- renameList = [];
1112
+ await Promise.allSettled(renameList
1113
+ .filter(([_, filePath]) => filePath)
1114
+ .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1120
1115
  if (this.tables[tableName].config.cache)
1121
1116
  await this.clearCache(tableName);
1122
1117
  this.totalItems[`${tableName}-*`] += Array.isArray(data)
1123
1118
  ? data.length
1124
1119
  : 1;
1125
- await writeFile(join(tablePath, ".cache", ".pagination"), `${lastId},${this.totalItems[`${tableName}-*`]}`);
1120
+ await rename(join(tablePath, paginationFilePath), join(tablePath, `${lastId}-${this.totalItems[`${tableName}-*`]}.pagination`));
1126
1121
  if (returnPostedData)
1127
1122
  return this.get(tableName, this.tables[tableName].config.prepend
1128
1123
  ? Array.isArray(data)
@@ -1144,7 +1139,7 @@ export default class Inibase {
1144
1139
  page: 1,
1145
1140
  perPage: 15,
1146
1141
  }, returnUpdatedData) {
1147
- let renameList = [];
1142
+ const renameList = [];
1148
1143
  const tablePath = join(this.databasePath, tableName);
1149
1144
  const schema = (await this.throwErrorIfTableEmpty(tableName))
1150
1145
  .schema;
@@ -1159,13 +1154,6 @@ export default class Inibase {
1159
1154
  throw this.Error("INVALID_ID", data.id);
1160
1155
  return this.put(tableName, data, data.id, options, returnUpdatedData || undefined);
1161
1156
  }
1162
- let totalItems;
1163
- if (await File.isExists(join(tablePath, ".cache", ".pagination")))
1164
- totalItems = (await readFile(join(tablePath, ".cache", ".pagination"), "utf8"))
1165
- .split(",")
1166
- .map(Number)[1];
1167
- else
1168
- totalItems = await File.count(join(tablePath, `id${this.getFileExtension(tableName)}`));
1169
1157
  this.validateData(data, schema, true);
1170
1158
  await this.checkUnique(tableName, schema);
1171
1159
  data = this.formatData(data, schema, true);
@@ -1175,13 +1163,16 @@ export default class Inibase {
1175
1163
  });
1176
1164
  try {
1177
1165
  await File.lock(join(tablePath, ".tmp"));
1178
- await Promise.all(Object.entries(pathesContents).map(async ([path, content]) => {
1179
- const replacementObject = {};
1180
- for (const index of [...Array(totalItems)].keys())
1181
- replacementObject[`${index + 1}`] = content;
1182
- renameList.push(await File.replace(path, replacementObject));
1183
- }));
1184
- await Promise.all(renameList.map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1166
+ for await (const paginationFilePath of glob("*.pagination", {
1167
+ cwd: tablePath,
1168
+ }))
1169
+ this.totalItems[`${tableName}-*`] = parse(paginationFilePath)
1170
+ .name.split("-")
1171
+ .map(Number)[1];
1172
+ await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content, this.totalItems[`${tableName}-*`]))));
1173
+ await Promise.allSettled(renameList
1174
+ .filter(([_, filePath]) => filePath)
1175
+ .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1185
1176
  if (this.tables[tableName].config.cache)
1186
1177
  await this.clearCache(join(tablePath, ".cache"));
1187
1178
  if (returnUpdatedData)
@@ -1220,9 +1211,10 @@ export default class Inibase {
1220
1211
  .join("."));
1221
1212
  try {
1222
1213
  await File.lock(join(tablePath, ".tmp"), keys);
1223
- await Promise.all(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content))));
1224
- await Promise.all(renameList.map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1225
- renameList = [];
1214
+ await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content))));
1215
+ await Promise.allSettled(renameList
1216
+ .filter(([_, filePath]) => filePath)
1217
+ .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1226
1218
  if (this.tables[tableName].config.cache)
1227
1219
  await this.clearCache(tableName);
1228
1220
  if (returnUpdatedData)
@@ -1236,7 +1228,8 @@ export default class Inibase {
1236
1228
  }
1237
1229
  else if (Utils.isObject(where)) {
1238
1230
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1239
- return this.put(tableName, data, lineNumbers, options, returnUpdatedData || undefined);
1231
+ if (lineNumbers)
1232
+ return this.put(tableName, data, lineNumbers, options, returnUpdatedData || undefined);
1240
1233
  }
1241
1234
  else
1242
1235
  throw this.Error("INVALID_PARAMETERS");
@@ -1254,19 +1247,20 @@ export default class Inibase {
1254
1247
  if (!where) {
1255
1248
  try {
1256
1249
  await File.lock(join(tablePath, ".tmp"));
1257
- let lastId;
1258
- if (await File.isExists(join(tablePath, ".cache", ".pagination")))
1259
- lastId = (await readFile(join(tablePath, ".cache", ".pagination"), "utf8"))
1260
- .split(",")
1261
- .map(Number)[0];
1262
- else
1263
- lastId = Number(Object.keys((await File.get(join(tablePath, `id${this.getFileExtension(tableName)}`), -1, "number", undefined, this.salt, true))?.[0] ?? 0));
1250
+ let paginationFilePath;
1251
+ let pagination;
1252
+ for await (const filePath of glob("*.pagination", {
1253
+ cwd: tablePath,
1254
+ })) {
1255
+ paginationFilePath = filePath;
1256
+ pagination = parse(filePath).name.split("-").map(Number);
1257
+ }
1264
1258
  await Promise.all((await readdir(tablePath))
1265
1259
  ?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)))
1266
1260
  .map(async (file) => unlink(join(tablePath, file))));
1267
1261
  if (this.tables[tableName].config.cache)
1268
1262
  await this.clearCache(tableName);
1269
- await writeFile(join(tablePath, ".cache", ".pagination"), `${lastId},0`);
1263
+ await rename(join(tablePath, paginationFilePath), join(tablePath, `${pagination[0]}-0.pagination`));
1270
1264
  return true;
1271
1265
  }
1272
1266
  finally {
@@ -1286,22 +1280,20 @@ export default class Inibase {
1286
1280
  const renameList = [];
1287
1281
  try {
1288
1282
  await File.lock(join(tablePath, ".tmp"));
1289
- let lastId;
1290
- if (await File.isExists(join(tablePath, ".cache", ".pagination")))
1291
- [lastId, this.totalItems[`${tableName}-*`]] = (await readFile(join(tablePath, ".cache", ".pagination"), "utf8"))
1292
- .split(",")
1293
- .map(Number);
1294
- else {
1295
- lastId = Number(Object.keys((await File.get(join(tablePath, `id${this.getFileExtension(tableName)}`), -1, "number", undefined, this.salt, true))?.[0] ?? 0));
1296
- if (!this.totalItems[`${tableName}-*`])
1297
- this.totalItems[`${tableName}-*`] = await File.count(join(tablePath, `id${this.getFileExtension(tableName)}`));
1283
+ let paginationFilePath;
1284
+ let pagination;
1285
+ for await (const filePath of glob("*.pagination", {
1286
+ cwd: tablePath,
1287
+ })) {
1288
+ paginationFilePath = filePath;
1289
+ pagination = parse(filePath).name.split("-").map(Number);
1298
1290
  }
1299
- if (this.totalItems[`${tableName}-*`] &&
1300
- this.totalItems[`${tableName}-*`] -
1301
- (Array.isArray(where) ? where.length : 1) >
1302
- 0) {
1291
+ if (pagination[1] &&
1292
+ pagination[1] - (Array.isArray(where) ? where.length : 1) > 0) {
1303
1293
  await Promise.all(files.map(async (file) => renameList.push(await File.remove(join(tablePath, file), where))));
1304
- await Promise.all(renameList.map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1294
+ await Promise.all(renameList
1295
+ .filter(([_, filePath]) => filePath)
1296
+ .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1305
1297
  }
1306
1298
  else
1307
1299
  await Promise.all((await readdir(tablePath))
@@ -1309,8 +1301,7 @@ export default class Inibase {
1309
1301
  .map(async (file) => unlink(join(tablePath, file))));
1310
1302
  if (this.tables[tableName].config.cache)
1311
1303
  await this.clearCache(tableName);
1312
- await writeFile(join(tablePath, ".cache", ".pagination"), `${lastId},${this.totalItems[`${tableName}-*`] -
1313
- (Array.isArray(where) ? where.length : 1)}`);
1304
+ await rename(join(tablePath, paginationFilePath), join(tablePath, `${pagination[0]}-${pagination[1] - (Array.isArray(where) ? where.length : 1)}.pagination`));
1314
1305
  return true;
1315
1306
  }
1316
1307
  finally {
@@ -1322,7 +1313,8 @@ export default class Inibase {
1322
1313
  }
1323
1314
  else if (Utils.isObject(where)) {
1324
1315
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1325
- return this.delete(tableName, lineNumbers);
1316
+ if (lineNumbers)
1317
+ return this.delete(tableName, lineNumbers);
1326
1318
  }
1327
1319
  else
1328
1320
  throw this.Error("INVALID_PARAMETERS");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inibase",
3
- "version": "1.0.0-rc.101",
3
+ "version": "1.0.0-rc.103",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Karim Amahtil",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "description": "A file-based & memory-efficient, serverless, ACID compliant, relational database management system",
21
21
  "engines": {
22
- "node": ">=16"
22
+ "node": ">=22"
23
23
  },
24
24
  "files": [
25
25
  "/dist"