inibase 1.0.0-rc.125 → 1.0.0-rc.126

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.js CHANGED
@@ -1,11 +1,9 @@
1
- import { createWriteStream } from "node:fs";
2
1
  import { access, appendFile, copyFile, constants as fsConstants, open, readFile, unlink, writeFile, } from "node:fs/promises";
3
2
  import { join, resolve } from "node:path";
4
3
  import { createInterface } from "node:readline";
5
4
  import { Transform } from "node:stream";
6
5
  import { pipeline } from "node:stream/promises";
7
6
  import { createGunzip, createGzip } from "node:zlib";
8
- import { spawn as spawnSync } from "node:child_process";
9
7
  import Inison from "inison";
10
8
  import { detectFieldType, isArrayOfObjects, isStringified, isNumber, isObject, } from "./utils.js";
11
9
  import { compare, encodeID, exec, gunzip, gzip } from "./utils.server.js";
@@ -275,9 +273,9 @@ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, s
275
273
  export const replace = async (filePath, replacements, totalItems) => {
276
274
  const fileTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/$1");
277
275
  const isReplacementsObject = isObject(replacements);
278
- const isReplacementsObjectHasLineNumbers = isReplacementsObject && !Number.isNaN(Number(Object.keys(replacements)[0]));
276
+ const isReplacementsLineNumbered = isReplacementsObject && !Number.isNaN(Number(Object.keys(replacements)[0]));
279
277
  if (await isExists(filePath)) {
280
- if (isReplacementsObjectHasLineNumbers) {
278
+ if (isReplacementsLineNumbered) {
281
279
  let fileHandle = null;
282
280
  let fileTempHandle = null;
283
281
  try {
@@ -328,34 +326,24 @@ export const replace = async (filePath, replacements, totalItems) => {
328
326
  }
329
327
  }
330
328
  else {
331
- return new Promise((resolve) => {
332
- const sedProcess = spawnSync("sed", [
333
- "-e",
334
- `s/.*/${replacements}/`,
335
- "-e",
336
- `/^$/s/^/${replacements}/`,
337
- filePath,
338
- ]);
339
- const outputStream = createWriteStream(fileTempPath); // Temp file for output
340
- // Pipe sed output to the temporary file
341
- sedProcess.stdout.pipe(outputStream);
342
- // Handle the process close
343
- sedProcess.on("close", (code) => {
344
- if (code === 0)
345
- resolve([fileTempPath, filePath]);
346
- else
347
- resolve([fileTempPath, null]);
348
- });
349
- // Handle errors in spawning the sed process
350
- sedProcess.on("error", () => {
351
- resolve([fileTempPath, null]);
352
- });
353
- });
329
+ const escapedFilePath = escapeShellPath(filePath);
330
+ const escapedFileTempPath = escapeShellPath(fileTempPath);
331
+ const sedCommand = `sed -e s/.*/${replacements}/ -e /^$/s/^/${replacements}/ ${escapedFilePath}`;
332
+ const command = filePath.endsWith(".gz")
333
+ ? `zcat ${escapedFilePath} | ${sedCommand} | gzip > ${escapedFileTempPath}`
334
+ : `${sedCommand} > ${escapedFileTempPath}`;
335
+ try {
336
+ await exec(command);
337
+ return [fileTempPath, filePath];
338
+ }
339
+ catch {
340
+ return [fileTempPath, null];
341
+ }
354
342
  }
355
343
  }
356
344
  else if (isReplacementsObject) {
357
345
  try {
358
- if (isReplacementsObjectHasLineNumbers) {
346
+ if (isReplacementsLineNumbered) {
359
347
  const replacementsKeys = Object.keys(replacements)
360
348
  .map(Number)
361
349
  .toSorted((a, b) => a - b);
@@ -542,7 +530,8 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
542
530
  // Increment the line count for each line.
543
531
  linesCount++;
544
532
  // Search only in provided linesNumbers
545
- if (searchIn?.size && (!searchIn.has(linesCount) || searchIn.has(-linesCount)))
533
+ if (searchIn?.size &&
534
+ (!searchIn.has(linesCount) || searchIn.has(-linesCount)))
546
535
  continue;
547
536
  // Decode the line for comparison.
548
537
  const decodedLine = decode(line, fieldType, fieldChildrenType, secretKey);
package/dist/index.d.ts CHANGED
@@ -62,7 +62,7 @@ export default class Inibase {
62
62
  private totalItems;
63
63
  constructor(database: string, mainFolder?: string);
64
64
  private static errorMessages;
65
- createError(code: ErrorCodes, variable?: string | number | (string | number)[], language?: ErrorLang): Error;
65
+ createError(name: ErrorCodes, variable?: string | number | (string | number)[], language?: ErrorLang): Error;
66
66
  clear(): void;
67
67
  private getFileExtension;
68
68
  private _schemaToIdsPath;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "dotenv/config";
2
- import { randomBytes, randomInt, scryptSync } from "node:crypto";
2
+ import { randomBytes, scryptSync } from "node:crypto";
3
3
  import { appendFileSync, existsSync, readFileSync } from "node:fs";
4
4
  import { glob, mkdir, readFile, readdir, rename, rm, unlink, writeFile, } from "node:fs/promises";
5
5
  import { join, parse } from "node:path";
@@ -48,15 +48,17 @@ export default class Inibase {
48
48
  : "please use dotenv",
49
49
  },
50
50
  };
51
- createError(code, variable, language = "en") {
52
- const errorMessage = Inibase.errorMessages[language]?.[code];
51
+ createError(name, variable, language = "en") {
52
+ const errorMessage = Inibase.errorMessages[language]?.[name];
53
53
  if (!errorMessage)
54
54
  return new Error("ERR");
55
- return new Error(variable
55
+ const error = new Error(variable
56
56
  ? Array.isArray(variable)
57
57
  ? errorMessage.replace(/\{variable\}/g, () => variable.shift()?.toString() ?? "")
58
58
  : errorMessage.replaceAll("{variable}", `'${variable.toString()}'`)
59
59
  : errorMessage.replaceAll("{variable}", ""));
60
+ error.name = name;
61
+ return error;
60
62
  }
61
63
  clear() {
62
64
  this.tablesMap = new Map();
@@ -293,10 +295,12 @@ export default class Inibase {
293
295
  throw this.createError("TABLE_EMPTY", tableName);
294
296
  }
295
297
  _validateData(data, schema, skipRequiredField = false) {
296
- if (Utils.isArrayOfObjects(data))
298
+ if (Utils.isArrayOfObjects(data)) {
297
299
  for (const single_data of data)
298
300
  this._validateData(single_data, schema, skipRequiredField);
299
- else if (Utils.isObject(data)) {
301
+ return;
302
+ }
303
+ if (Utils.isObject(data)) {
300
304
  for (const field of schema) {
301
305
  if (!Object.hasOwn(data, field.key) ||
302
306
  data[field.key] === null ||
@@ -304,7 +308,7 @@ export default class Inibase {
304
308
  data[field.key] === "") {
305
309
  if (field.required && !skipRequiredField)
306
310
  throw this.createError("FIELD_REQUIRED", field.key);
307
- return;
311
+ continue;
308
312
  }
309
313
  if (!Utils.validateFieldType(data[field.key], field.type, (field.type === "array" || field.type === "object") &&
310
314
  field.children &&
@@ -313,7 +317,12 @@ export default class Inibase {
313
317
  : undefined))
314
318
  throw this.createError("INVALID_TYPE", [
315
319
  field.key,
316
- Array.isArray(field.type) ? field.type.join(", ") : field.type,
320
+ (Array.isArray(field.type) ? field.type.join(", ") : field.type) +
321
+ (Array.isArray(field.children)
322
+ ? Utils.isArrayOfObjects(field.children)
323
+ ? "[object]"
324
+ : `[${field.children.join("|")}]`
325
+ : `[${field.children}]`),
317
326
  data[field.key],
318
327
  ]);
319
328
  if ((field.type === "array" || field.type === "object") &&
@@ -329,7 +338,7 @@ export default class Inibase {
329
338
  if (field.unique) {
330
339
  let uniqueKey;
331
340
  if (typeof field.unique === "boolean")
332
- uniqueKey = randomInt(55);
341
+ uniqueKey = field.id;
333
342
  else
334
343
  uniqueKey = field.unique;
335
344
  if (!this.uniqueMap.has(uniqueKey))
@@ -433,22 +442,38 @@ export default class Inibase {
433
442
  async checkUnique(tableName) {
434
443
  const tablePath = join(this.databasePath, tableName);
435
444
  const flattenSchema = Utils.flattenSchema(this.tablesMap.get(tableName).schema);
445
+ function hasDuplicates(setA, setB) {
446
+ for (const value of setA)
447
+ if (setB.has(value))
448
+ return true; // Stop and return true if a duplicate is found
449
+ return false; // No duplicates found
450
+ }
436
451
  for await (const [_uniqueID, valueObject] of this.uniqueMap) {
437
452
  let index = 0;
453
+ let shouldContinueParent = false; // Flag to manage parent loop continuation
454
+ const mergedLineNumbers = new Set();
438
455
  for await (const [columnID, values] of valueObject.columnsValues) {
439
456
  index++;
440
457
  const field = flattenSchema.find(({ id }) => id === columnID);
441
- const [searchResult, totalLines] = await File.search(join(tablePath, `${field.key}${this.getFileExtension(tableName)}`), "[]", Array.from(values), undefined, valueObject.exclude, field.type, field.children, 1, undefined, false, this.salt);
458
+ const [searchResult, totalLines, lineNumbers] = await File.search(join(tablePath, `${field.key}${this.getFileExtension(tableName)}`), "[]", Array.from(values), undefined, valueObject.exclude, field.type, field.children, 1, undefined, false, this.salt);
442
459
  if (searchResult && totalLines > 0) {
443
- if (index === valueObject.columnsValues.size)
460
+ if (valueObject.columnsValues.size === 1 ||
461
+ hasDuplicates(lineNumbers, mergedLineNumbers)) {
462
+ this.uniqueMap = new Map();
444
463
  throw this.createError("FIELD_UNIQUE", [
445
464
  field.key,
446
465
  Array.from(values).join(", "),
447
466
  ]);
467
+ }
468
+ lineNumbers.forEach(mergedLineNumbers.add, mergedLineNumbers);
469
+ }
470
+ else {
471
+ shouldContinueParent = true; // Flag to skip the rest of this inner loop
472
+ break; // Exit the inner loop
448
473
  }
449
- else
450
- return;
451
474
  }
475
+ if (shouldContinueParent)
476
+ continue;
452
477
  }
453
478
  this.uniqueMap = new Map();
454
479
  }
@@ -1165,8 +1190,9 @@ export default class Inibase {
1165
1190
  throw this.createError("NO_SCHEMA", tableName);
1166
1191
  if (!returnPostedData)
1167
1192
  returnPostedData = false;
1168
- const keys = UtilsServer.hashString(Object.keys(Array.isArray(data) ? data[0] : data).join("."));
1169
- await this.validateData(tableName, data);
1193
+ let clonedData = JSON.parse(JSON.stringify(data));
1194
+ const keys = UtilsServer.hashString(Object.keys(Array.isArray(clonedData) ? clonedData[0] : clonedData).join("."));
1195
+ await this.validateData(tableName, clonedData);
1170
1196
  const renameList = [];
1171
1197
  try {
1172
1198
  await File.lock(join(tablePath, ".tmp"), keys);
@@ -1177,24 +1203,24 @@ export default class Inibase {
1177
1203
  .name.split("-")
1178
1204
  .map(Number);
1179
1205
  this.totalItems.set(`${tableName}-*`, _totalItems);
1180
- if (Utils.isArrayOfObjects(data))
1181
- for (let index = 0; index < data.length; index++) {
1182
- const element = data[index];
1206
+ if (Utils.isArrayOfObjects(clonedData))
1207
+ for (let index = 0; index < clonedData.length; index++) {
1208
+ const element = clonedData[index];
1183
1209
  element.id = ++lastId;
1184
1210
  element.createdAt = Date.now();
1185
1211
  element.updatedAt = undefined;
1186
1212
  }
1187
1213
  else {
1188
- data.id = ++lastId;
1189
- data.createdAt = Date.now();
1190
- data.updatedAt = undefined;
1214
+ clonedData.id = ++lastId;
1215
+ clonedData.createdAt = Date.now();
1216
+ clonedData.updatedAt = undefined;
1191
1217
  }
1192
- data = this.formatData(data, this.tablesMap.get(tableName).schema, true);
1218
+ clonedData = this.formatData(clonedData, this.tablesMap.get(tableName).schema, true);
1193
1219
  const pathesContents = this.joinPathesContents(tableName, this.tablesMap.get(tableName).config.prepend
1194
- ? Array.isArray(data)
1195
- ? data.toReversed()
1196
- : data
1197
- : data);
1220
+ ? Array.isArray(clonedData)
1221
+ ? clonedData.toReversed()
1222
+ : clonedData
1223
+ : clonedData);
1198
1224
  await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(this.tablesMap.get(tableName).config.prepend
1199
1225
  ? await File.prepend(path, content)
1200
1226
  : await File.append(path, content))));
@@ -1208,14 +1234,14 @@ export default class Inibase {
1208
1234
  await rename(paginationFilePath, join(tablePath, `${lastId}-${this.totalItems.get(`${tableName}-*`)}.pagination`));
1209
1235
  if (returnPostedData)
1210
1236
  return this.get(tableName, this.tablesMap.get(tableName).config.prepend
1211
- ? Array.isArray(data)
1212
- ? data.map((_, index) => index + 1).toReversed()
1237
+ ? Array.isArray(clonedData)
1238
+ ? clonedData.map((_, index) => index + 1).toReversed()
1213
1239
  : 1
1214
- : Array.isArray(data)
1215
- ? data
1240
+ : Array.isArray(clonedData)
1241
+ ? clonedData
1216
1242
  .map((_, index) => this.totalItems.get(`${tableName}-*`) - index)
1217
1243
  .toReversed()
1218
- : this.totalItems.get(`${tableName}-*`), options, !Utils.isArrayOfObjects(data));
1244
+ : this.totalItems.get(`${tableName}-*`), options, !Utils.isArrayOfObjects(clonedData));
1219
1245
  }
1220
1246
  finally {
1221
1247
  if (renameList.length)
@@ -1230,21 +1256,22 @@ export default class Inibase {
1230
1256
  const renameList = [];
1231
1257
  const tablePath = join(this.databasePath, tableName);
1232
1258
  await this.throwErrorIfTableEmpty(tableName);
1259
+ let clonedData = JSON.parse(JSON.stringify(data));
1233
1260
  if (!where) {
1234
- if (Utils.isArrayOfObjects(data)) {
1235
- if (!data.every((item) => Object.hasOwn(item, "id") && Utils.isValidID(item.id)))
1261
+ if (Utils.isArrayOfObjects(clonedData)) {
1262
+ if (!clonedData.every((item) => Object.hasOwn(item, "id") && Utils.isValidID(item.id)))
1236
1263
  throw this.createError("INVALID_ID");
1237
- return this.put(tableName, data, data.map(({ id }) => id), options, returnUpdatedData || undefined);
1264
+ return this.put(tableName, clonedData, clonedData.map(({ id }) => id), options, returnUpdatedData || undefined);
1238
1265
  }
1239
1266
  if (Object.hasOwn(data, "id")) {
1240
- if (!Utils.isValidID(data.id))
1241
- throw this.createError("INVALID_ID", data.id);
1242
- return this.put(tableName, data, data.id, options, returnUpdatedData || undefined);
1267
+ if (!Utils.isValidID(clonedData.id))
1268
+ throw this.createError("INVALID_ID", clonedData.id);
1269
+ return this.put(tableName, data, clonedData.id, options, returnUpdatedData || undefined);
1243
1270
  }
1244
- await this.validateData(tableName, data, true);
1245
- data = this.formatData(data, this.tablesMap.get(tableName).schema, true);
1271
+ await this.validateData(tableName, clonedData, true);
1272
+ clonedData = this.formatData(clonedData, this.tablesMap.get(tableName).schema, true);
1246
1273
  const pathesContents = this.joinPathesContents(tableName, {
1247
- ...(({ id, ...restOfData }) => restOfData)(data),
1274
+ ...(({ id, ...restOfData }) => restOfData)(clonedData),
1248
1275
  updatedAt: Date.now(),
1249
1276
  });
1250
1277
  try {
@@ -1271,19 +1298,19 @@ export default class Inibase {
1271
1298
  else if ((Array.isArray(where) && where.every(Utils.isValidID)) ||
1272
1299
  Utils.isValidID(where)) {
1273
1300
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1274
- return this.put(tableName, data, lineNumbers, options, returnUpdatedData || undefined);
1301
+ return this.put(tableName, clonedData, lineNumbers, options, returnUpdatedData || undefined);
1275
1302
  }
1276
1303
  else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1277
1304
  Utils.isNumber(where)) {
1278
1305
  // "where" in this case, is the line(s) number(s) and not id(s)
1279
- await this.validateData(tableName, data, true);
1280
- data = this.formatData(data, this.tablesMap.get(tableName).schema, true);
1281
- const pathesContents = Object.fromEntries(Object.entries(this.joinPathesContents(tableName, Utils.isArrayOfObjects(data)
1282
- ? data.map((item) => ({
1306
+ await this.validateData(tableName, clonedData, true);
1307
+ clonedData = this.formatData(clonedData, this.tablesMap.get(tableName).schema, true);
1308
+ const pathesContents = Object.fromEntries(Object.entries(this.joinPathesContents(tableName, Utils.isArrayOfObjects(clonedData)
1309
+ ? clonedData.map((item) => ({
1283
1310
  ...item,
1284
1311
  updatedAt: Date.now(),
1285
1312
  }))
1286
- : { ...data, updatedAt: Date.now() })).map(([path, content]) => [
1313
+ : { ...clonedData, updatedAt: Date.now() })).map(([path, content]) => [
1287
1314
  path,
1288
1315
  [...(Array.isArray(where) ? where : [where])].reduce((obj, lineNum, index) => Object.assign(obj, {
1289
1316
  [lineNum]: Array.isArray(content) ? content[index] : content,
@@ -1312,7 +1339,7 @@ export default class Inibase {
1312
1339
  else if (Utils.isObject(where)) {
1313
1340
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1314
1341
  if (lineNumbers)
1315
- return this.put(tableName, data, lineNumbers, options, returnUpdatedData || undefined);
1342
+ return this.put(tableName, clonedData, lineNumbers, options, returnUpdatedData || undefined);
1316
1343
  }
1317
1344
  else
1318
1345
  throw this.createError("INVALID_PARAMETERS");
package/dist/utils.js CHANGED
@@ -318,16 +318,17 @@ export const validateFieldType = (value, fieldType, fieldChildrenType) => {
318
318
  fieldType = detectedFieldType;
319
319
  }
320
320
  if (fieldType === "array" && fieldChildrenType)
321
- return value.every((v) => {
322
- let _fieldChildrenType = fieldChildrenType;
323
- if (Array.isArray(_fieldChildrenType)) {
324
- const detectedFieldType = detectFieldType(v, _fieldChildrenType);
325
- if (!detectedFieldType)
326
- return false;
327
- _fieldChildrenType = detectedFieldType;
328
- }
329
- return validateFieldType(v, _fieldChildrenType);
330
- });
321
+ return (Array.isArray(value) &&
322
+ value.every((v) => {
323
+ let _fieldChildrenType = fieldChildrenType;
324
+ if (Array.isArray(_fieldChildrenType)) {
325
+ const detectedFieldType = detectFieldType(v, _fieldChildrenType);
326
+ if (!detectedFieldType)
327
+ return false;
328
+ _fieldChildrenType = detectedFieldType;
329
+ }
330
+ return validateFieldType(v, _fieldChildrenType);
331
+ }));
331
332
  switch (fieldType) {
332
333
  case "string":
333
334
  return isString(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inibase",
3
- "version": "1.0.0-rc.125",
3
+ "version": "1.0.0-rc.126",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Karim Amahtil",
@@ -68,6 +68,7 @@
68
68
  }
69
69
  },
70
70
  "devDependencies": {
71
+ "@biomejs/biome": "1.9.4",
71
72
  "@types/bun": "^1.1.10",
72
73
  "@types/node": "^22.7.4",
73
74
  "tinybench": "^3.0.7",
@@ -81,6 +82,7 @@
81
82
  "scripts": {
82
83
  "prepublish": "npx tsc",
83
84
  "build": "npx tsc",
84
- "benchmark": "./benchmark/run.js"
85
+ "benchmark": "./benchmark/run.js",
86
+ "test": "npx tsx ./tests/inibase.test.ts"
85
87
  }
86
88
  }