inibase 1.5.10 → 1.6.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/cli.js CHANGED
@@ -7,7 +7,8 @@ import { parseArgs } from "node:util";
7
7
  import Inison from "inison";
8
8
  import { isExists } from "./file.js";
9
9
  import Inibase, {} from "./index.js";
10
- import { isNumber, isStringified, setField, unsetField } from "./utils.js";
10
+ import { isNumber, isStringified, isValidName, setField, unsetField, } from "./utils.js";
11
+ const isSafeName = (input) => isValidName(input);
11
12
  const textGreen = (input) => `\u001b[1;32m${input}\u001b[0m`;
12
13
  const textRed = (input) => `\u001b[1;31m${input}\u001b[0m`;
13
14
  const textBlue = (input) => `\u001b[1;34m${input}\u001b[0m`;
@@ -178,7 +179,9 @@ rl.on("line", async (input) => {
178
179
  console.log(`${textRed(" Err:")} Please specify table name`);
179
180
  break;
180
181
  }
181
- if (!(await isExists(join(path, splitedInput[1]))))
182
+ if (!isSafeName(splitedInput[1]))
183
+ console.log(`${textRed(" Err:")} Invalid table name, only alphanumeric characters, underscores and hyphens are allowed`);
184
+ else if (!(await isExists(join(path, splitedInput[1]))))
182
185
  console.log(`${textRed(" Err:")} Table doesn't exist`);
183
186
  else {
184
187
  table = splitedInput[1];
package/dist/file.js CHANGED
@@ -8,6 +8,8 @@ import Inison from "inison";
8
8
  import { globalConfig, } from "./index.js";
9
9
  import { detectFieldType, isArrayOfObjects, isNumber, isObject, isStringified, } from "./utils.js";
10
10
  import { compare, encodeID, exec, gunzip, gzip } from "./utils.server.js";
11
+ // Locks older than this are assumed abandoned by a crashed/killed process, not a slow operation.
12
+ const STALE_LOCK_MS = 30_000;
11
13
  export const lock = async (folderPath, prefix) => {
12
14
  let lockFile = null;
13
15
  const lockFilePath = join(folderPath, `${prefix ?? ""}.locked`);
@@ -16,8 +18,12 @@ export const lock = async (folderPath, prefix) => {
16
18
  return;
17
19
  }
18
20
  catch ({ message }) {
19
- if (message.split(":")[0] === "EEXIST")
21
+ if (message.split(":")[0] === "EEXIST") {
22
+ const lockStat = await stat(lockFilePath).catch(() => null);
23
+ if (lockStat && Date.now() - lockStat.mtimeMs > STALE_LOCK_MS)
24
+ await unlink(lockFilePath).catch(() => { });
20
25
  return await new Promise((resolve) => setTimeout(() => resolve(lock(folderPath, prefix)), 13));
26
+ }
21
27
  }
22
28
  finally {
23
29
  await lockFile?.close();
package/dist/index.d.ts CHANGED
@@ -51,7 +51,7 @@ declare global {
51
51
  entries<T extends object>(o: T): Entries<T>;
52
52
  }
53
53
  }
54
- export declare const ERROR_CODES: readonly ["GROUP_UNIQUE", "FIELD_UNIQUE", "FIELD_REQUIRED", "NO_SCHEMA", "TABLE_EMPTY", "INVALID_ID", "INVALID_TYPE", "INVALID_PARAMETERS", "NO_ENV", "TABLE_EXISTS", "TABLE_NOT_EXISTS", "INVALID_REGEX_MATCH"];
54
+ export declare const ERROR_CODES: readonly ["GROUP_UNIQUE", "FIELD_UNIQUE", "FIELD_REQUIRED", "NO_SCHEMA", "TABLE_EMPTY", "INVALID_ID", "INVALID_TYPE", "INVALID_PARAMETERS", "NO_ENV", "TABLE_EXISTS", "TABLE_NOT_EXISTS", "INVALID_REGEX_MATCH", "INVALID_NAME"];
55
55
  export type ErrorCode = (typeof ERROR_CODES)[number];
56
56
  export type ErrorLang = "en" | "ar" | "fr" | "es";
57
57
  export declare const globalConfig: {
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ export const ERROR_CODES = [
21
21
  "TABLE_EXISTS",
22
22
  "TABLE_NOT_EXISTS",
23
23
  "INVALID_REGEX_MATCH",
24
+ "INVALID_NAME",
24
25
  ];
25
26
  // hide ExperimentalWarning glob()
26
27
  process.removeAllListeners("warning");
@@ -39,6 +40,7 @@ export default class Inibase {
39
40
  uniqueMap;
40
41
  schemaFileExtension = process.env.INIBASE_SCHEMA_EXTENSION ?? "json";
41
42
  constructor(database, mainFolder = ".", language = "en") {
43
+ Utils.validateName(database);
42
44
  this.databasePath = join(mainFolder, database);
43
45
  this.language = language;
44
46
  this.pageInfo = {};
@@ -69,6 +71,7 @@ export default class Inibase {
69
71
  INVALID_TYPE: "Expect {variable} to be {variable}, got {variable} instead",
70
72
  INVALID_PARAMETERS: "The given parameters are not valid",
71
73
  INVALID_REGEX_MATCH: "Field {variable} does not match the expected pattern",
74
+ INVALID_NAME: "Name {variable} is not valid",
72
75
  NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
73
76
  ? "please run with '--env-file=.env'"
74
77
  : "please use dotenv",
@@ -85,6 +88,7 @@ export default class Inibase {
85
88
  INVALID_TYPE: "من المتوقع أن يكون {variable} من النوع {variable}، لكن تم العثور على {variable} بدلاً من ذلك",
86
89
  INVALID_PARAMETERS: "المعلمات المقدمة غير صالحة",
87
90
  INVALID_REGEX_MATCH: "الحقل {variable} لا يتطابق مع النمط المتوقع",
91
+ INVALID_NAME: "الاسم {variable} غير صالح",
88
92
  NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
89
93
  ? "يرجى التشغيل باستخدام '--env-file=.env'"
90
94
  : "يرجى استخدام dotenv",
@@ -101,6 +105,7 @@ export default class Inibase {
101
105
  INVALID_TYPE: "Attendu que {variable} soit de type {variable}, mais trouvé {variable} à la place",
102
106
  INVALID_PARAMETERS: "Les paramètres donnés ne sont pas valides",
103
107
  INVALID_REGEX_MATCH: "Le champ {variable} ne correspond pas au modèle attendu",
108
+ INVALID_NAME: "Le nom {variable} n'est pas valide",
104
109
  NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
105
110
  ? "veuillez exécuter avec '--env-file=.env'"
106
111
  : "veuillez utiliser dotenv",
@@ -117,6 +122,7 @@ export default class Inibase {
117
122
  INVALID_TYPE: "Se espera que {variable} sea {variable}, pero se encontró {variable} en su lugar",
118
123
  INVALID_PARAMETERS: "Los parámetros proporcionados no son válidos",
119
124
  INVALID_REGEX_MATCH: "El campo {variable} no coincide con el patrón esperado",
125
+ INVALID_NAME: "El nombre {variable} no es válido",
120
126
  NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
121
127
  ? "por favor ejecute con '--env-file=.env'"
122
128
  : "por favor use dotenv",
@@ -163,6 +169,9 @@ export default class Inibase {
163
169
  * @param {TableConfig} [config]
164
170
  */
165
171
  async createTable(tableName, schema, config) {
172
+ Utils.validateName(tableName);
173
+ if (schema)
174
+ Utils.validateSchema(schema);
166
175
  const tablePath = join(this.databasePath, tableName);
167
176
  if (await File.isExists(tablePath))
168
177
  throw this.createError("TABLE_EXISTS", tableName);
@@ -211,11 +220,15 @@ export default class Inibase {
211
220
  * @param {(TableConfig&{name?: string})} [config]
212
221
  */
213
222
  async updateTable(tableName, schema, config) {
223
+ Utils.validateName(tableName);
224
+ if (config?.name)
225
+ Utils.validateName(config.name);
214
226
  const table = await this.getTable(tableName);
215
227
  if (!table)
216
228
  return;
217
229
  const tablePath = join(this.databasePath, tableName);
218
230
  if (schema) {
231
+ Utils.validateSchema(schema);
219
232
  // remove id from schema
220
233
  schema = schema.filter(({ key }) => !["id", "createdAt", "updatedAt"].includes(key));
221
234
  let schemaIdFilePath = "";
@@ -330,6 +343,7 @@ export default class Inibase {
330
343
  * @return {*} {Promise<TableObject | undefined>}
331
344
  */
332
345
  async getTable(tableName) {
346
+ Utils.validateName(tableName);
333
347
  const tablePath = join(this.databasePath, tableName);
334
348
  if (!(await File.isExists(tablePath)))
335
349
  throw this.createError("TABLE_NOT_EXISTS", tableName);
@@ -349,6 +363,7 @@ export default class Inibase {
349
363
  return globalConfig[this.databasePath].tables?.get(tableName);
350
364
  }
351
365
  async getTableSchema(tableName) {
366
+ Utils.validateName(tableName);
352
367
  const tablePath = join(this.databasePath, tableName);
353
368
  let schemaFile;
354
369
  let schema;
@@ -1151,6 +1166,7 @@ export default class Inibase {
1151
1166
  * @param {string} tableName
1152
1167
  */
1153
1168
  async clearCache(tableName) {
1169
+ Utils.validateName(tableName);
1154
1170
  const cacheFolderPath = join(this.databasePath, tableName, ".cache");
1155
1171
  await rm(cacheFolderPath, { recursive: true, force: true });
1156
1172
  await mkdir(cacheFolderPath);
@@ -1159,12 +1175,15 @@ export default class Inibase {
1159
1175
  page: 1,
1160
1176
  perPage: 15,
1161
1177
  }, onlyOne, onlyLinesNumbers, _whereIsLinesNumbers) {
1178
+ Utils.validateName(tableName);
1162
1179
  const tablePath = join(this.databasePath, tableName);
1163
1180
  // Ensure options.columns is an array
1164
1181
  if (options.columns) {
1165
1182
  options.columns = Array.isArray(options.columns)
1166
1183
  ? options.columns
1167
1184
  : [options.columns];
1185
+ for (const column of options.columns)
1186
+ Utils.validateName(column);
1168
1187
  if (options.columns.length && !options.columns.includes("id"))
1169
1188
  options.columns.push("id");
1170
1189
  }
@@ -1407,6 +1426,12 @@ export default class Inibase {
1407
1426
  page: 1,
1408
1427
  perPage: 15,
1409
1428
  };
1429
+ Utils.validateName(tableName);
1430
+ if (options.columns)
1431
+ for (const column of (Array.isArray(options.columns)
1432
+ ? options.columns
1433
+ : [options.columns]))
1434
+ Utils.validateName(column);
1410
1435
  const tablePath = join(this.databasePath, tableName);
1411
1436
  await this.getTable(tableName);
1412
1437
  if (!globalConfig[this.databasePath].tables?.get(tableName)?.schema)
@@ -1487,6 +1512,12 @@ export default class Inibase {
1487
1512
  perPage: 15,
1488
1513
  }, returnUpdatedData, _whereIsLinesNumbers) {
1489
1514
  const renameList = [];
1515
+ Utils.validateName(tableName);
1516
+ if (options.columns)
1517
+ for (const column of (Array.isArray(options.columns)
1518
+ ? options.columns
1519
+ : [options.columns]))
1520
+ Utils.validateName(column);
1490
1521
  const tablePath = join(this.databasePath, tableName);
1491
1522
  await this.throwErrorIfTableEmpty(tableName);
1492
1523
  let clonedData = structuredClone(data);
@@ -1601,6 +1632,7 @@ export default class Inibase {
1601
1632
  * @return {boolean | null} {(Promise<boolean | null>)}
1602
1633
  */
1603
1634
  async delete(tableName, where, _whereIsLinesNumbers) {
1635
+ Utils.validateName(tableName);
1604
1636
  const tablePath = join(this.databasePath, tableName);
1605
1637
  await this.throwErrorIfTableEmpty(tableName);
1606
1638
  if (!where) {
@@ -1694,11 +1726,14 @@ export default class Inibase {
1694
1726
  return false;
1695
1727
  }
1696
1728
  async sum(tableName, columns, where) {
1697
- const RETURN = {};
1698
- const tablePath = join(this.databasePath, tableName);
1699
- await this.throwErrorIfTableEmpty(tableName);
1729
+ Utils.validateName(tableName);
1700
1730
  if (!Array.isArray(columns))
1701
1731
  columns = [columns];
1732
+ for (const column of columns)
1733
+ Utils.validateName(column);
1734
+ await this.throwErrorIfTableEmpty(tableName);
1735
+ const RETURN = {};
1736
+ const tablePath = join(this.databasePath, tableName);
1702
1737
  for await (const column of columns) {
1703
1738
  const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
1704
1739
  if (await File.isExists(columnPath)) {
@@ -1715,11 +1750,14 @@ export default class Inibase {
1715
1750
  return columns.length > 1 ? RETURN : Object.values(RETURN)[0];
1716
1751
  }
1717
1752
  async max(tableName, columns, where) {
1753
+ Utils.validateName(tableName);
1754
+ if (!Array.isArray(columns))
1755
+ columns = [columns];
1756
+ for (const column of columns)
1757
+ Utils.validateName(column);
1718
1758
  const RETURN = {};
1719
1759
  const tablePath = join(this.databasePath, tableName);
1720
1760
  await this.throwErrorIfTableEmpty(tableName);
1721
- if (!Array.isArray(columns))
1722
- columns = [columns];
1723
1761
  for await (const column of columns) {
1724
1762
  const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
1725
1763
  if (await File.isExists(columnPath)) {
@@ -1736,11 +1774,14 @@ export default class Inibase {
1736
1774
  return RETURN;
1737
1775
  }
1738
1776
  async min(tableName, columns, where) {
1777
+ Utils.validateName(tableName);
1778
+ if (!Array.isArray(columns))
1779
+ columns = [columns];
1780
+ for (const column of columns)
1781
+ Utils.validateName(column);
1739
1782
  const RETURN = {};
1740
1783
  const tablePath = join(this.databasePath, tableName);
1741
1784
  await this.throwErrorIfTableEmpty(tableName);
1742
- if (!Array.isArray(columns))
1743
- columns = [columns];
1744
1785
  for await (const column of columns) {
1745
1786
  const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
1746
1787
  if (await File.isExists(columnPath)) {
package/dist/utils.d.ts CHANGED
@@ -218,3 +218,33 @@ export declare const findLastIdNumber: (schema: Schema) => number;
218
218
  export declare function addIdToSchema(schema: Schema, startWithID: {
219
219
  value: number;
220
220
  }): Field[];
221
+ /**
222
+ * Validates that a string is a safe name for a table, database or column.
223
+ *
224
+ * Names must match `^[a-zA-Z0-9][a-zA-Z0-9_-]*$` (starts with an alphanumeric
225
+ * character, followed by alphanumerics, underscores or hyphens) and be at most
226
+ * 255 characters long.
227
+ *
228
+ * This prevents path traversal (`../`, `..\\`), null-byte injection (`\0`),
229
+ * shell injection (`;`, `|`, backticks, `$()`, spaces, slashes) and other
230
+ * reserved-name issues.
231
+ *
232
+ * @param input - The value to be checked.
233
+ * @returns boolean - True if the name is safe to use, false otherwise.
234
+ */
235
+ export declare const isValidName: (input: unknown) => input is string;
236
+ /**
237
+ * Validates that a string is a safe name for a table, database or column and
238
+ * throws an error if it is not.
239
+ *
240
+ * @param name - The name to validate.
241
+ * @throws {Error} If the name is not a safe name.
242
+ */
243
+ export declare const validateName: (name: string) => void;
244
+ /**
245
+ * Recursively validates every field key of a schema.
246
+ *
247
+ * @param schema - The schema to validate.
248
+ * @throws {Error} If any field key is not a safe name.
249
+ */
250
+ export declare const validateSchema: (schema: Schema) => void;
package/dist/utils.js CHANGED
@@ -628,3 +628,47 @@ export function addIdToSchema(schema, startWithID) {
628
628
  const addIdToSchemaHelper = (schema) => schema.map(addIdToField);
629
629
  return addIdToSchemaHelper(clonedSchema);
630
630
  }
631
+ /**
632
+ * Validates that a string is a safe name for a table, database or column.
633
+ *
634
+ * Names must match `^[a-zA-Z0-9][a-zA-Z0-9_-]*$` (starts with an alphanumeric
635
+ * character, followed by alphanumerics, underscores or hyphens) and be at most
636
+ * 255 characters long.
637
+ *
638
+ * This prevents path traversal (`../`, `..\\`), null-byte injection (`\0`),
639
+ * shell injection (`;`, `|`, backticks, `$()`, spaces, slashes) and other
640
+ * reserved-name issues.
641
+ *
642
+ * @param input - The value to be checked.
643
+ * @returns boolean - True if the name is safe to use, false otherwise.
644
+ */
645
+ export const isValidName = (input) => typeof input === "string" &&
646
+ input.length > 0 &&
647
+ input.length <= 255 &&
648
+ /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(input);
649
+ /**
650
+ * Validates that a string is a safe name for a table, database or column and
651
+ * throws an error if it is not.
652
+ *
653
+ * @param name - The name to validate.
654
+ * @throws {Error} If the name is not a safe name.
655
+ */
656
+ export const validateName = (name) => {
657
+ if (!isValidName(name))
658
+ throw new Error(`Invalid name: '${name}'. Names must contain only alphanumeric characters, underscores or hyphens, must start with an alphanumeric character and must be at most 255 characters long.`);
659
+ };
660
+ /**
661
+ * Recursively validates every field key of a schema.
662
+ *
663
+ * @param schema - The schema to validate.
664
+ * @throws {Error} If any field key is not a safe name.
665
+ */
666
+ export const validateSchema = (schema) => {
667
+ for (const field of schema) {
668
+ validateName(field.key);
669
+ if (field.table)
670
+ validateName(field.table);
671
+ if (field.children && isArrayOfObjects(field.children))
672
+ validateSchema(field.children);
673
+ }
674
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inibase",
3
- "version": "1.5.10",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Karim Amahtil",