inibase 1.5.11 → 1.6.1

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/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: {
@@ -77,8 +77,9 @@ export default class Inibase {
77
77
  private uniqueMap;
78
78
  private schemaFileExtension;
79
79
  constructor(database: string, mainFolder?: string, language?: ErrorLang);
80
- private static errorMessages;
81
80
  createError(name: ErrorCode, variable?: string | number | (string | number)[]): Error;
81
+ private validateName;
82
+ private validateSchema;
82
83
  private getFileExtension;
83
84
  private schemaToIdsPath;
84
85
  /**
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,8 +40,9 @@ export default class Inibase {
39
40
  uniqueMap;
40
41
  schemaFileExtension = process.env.INIBASE_SCHEMA_EXTENSION ?? "json";
41
42
  constructor(database, mainFolder = ".", language = "en") {
42
- this.databasePath = join(mainFolder, database);
43
43
  this.language = language;
44
+ this.validateName(database);
45
+ this.databasePath = join(mainFolder, database);
44
46
  this.pageInfo = {};
45
47
  this.totalItems = new Map();
46
48
  this.uniqueMap = new Map();
@@ -56,83 +58,14 @@ export default class Inibase {
56
58
  else
57
59
  globalConfig.salt = Buffer.from(process.env.INIBASE_SECRET, "hex");
58
60
  }
59
- static errorMessages = {
60
- en: {
61
- TABLE_EMPTY: "Table {variable} is empty",
62
- TABLE_EXISTS: "Table {variable} already exists",
63
- TABLE_NOT_EXISTS: "Table {variable} doesn't exist",
64
- NO_SCHEMA: "Table {variable} does't have a schema",
65
- GROUP_UNIQUE: "Group {variable} should be unique, got duplicated content in {variable}",
66
- FIELD_UNIQUE: "Field {variable} should be unique, got {variable} instead",
67
- FIELD_REQUIRED: "Field {variable} is required",
68
- INVALID_ID: "The given ID(s) is/are not valid(s)",
69
- INVALID_TYPE: "Expect {variable} to be {variable}, got {variable} instead",
70
- INVALID_PARAMETERS: "The given parameters are not valid",
71
- INVALID_REGEX_MATCH: "Field {variable} does not match the expected pattern",
72
- NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
73
- ? "please run with '--env-file=.env'"
74
- : "please use dotenv",
75
- },
76
- ar: {
77
- TABLE_EMPTY: "الجدول {variable} فارغ",
78
- TABLE_EXISTS: "الجدول {variable} موجود بالفعل",
79
- TABLE_NOT_EXISTS: "الجدول {variable} غير موجود",
80
- NO_SCHEMA: "الجدول {variable} ليس لديه مخطط",
81
- GROUP_UNIQUE: "المجموعة {variable} يجب أن تكون فريدة، تم العثور على محتوى مكرر في {variable}",
82
- FIELD_UNIQUE: "الحقل {variable} يجب أن يكون فريدًا، تم العثور على {variable} بدلاً من ذلك",
83
- FIELD_REQUIRED: "الحقل {variable} مطلوب",
84
- INVALID_ID: "المعرف أو المعرفات المقدمة غير صالحة",
85
- INVALID_TYPE: "من المتوقع أن يكون {variable} من النوع {variable}، لكن تم العثور على {variable} بدلاً من ذلك",
86
- INVALID_PARAMETERS: "المعلمات المقدمة غير صالحة",
87
- INVALID_REGEX_MATCH: "الحقل {variable} لا يتطابق مع النمط المتوقع",
88
- NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
89
- ? "يرجى التشغيل باستخدام '--env-file=.env'"
90
- : "يرجى استخدام dotenv",
91
- },
92
- fr: {
93
- TABLE_EMPTY: "La table {variable} est vide",
94
- TABLE_EXISTS: "La table {variable} existe déjà",
95
- TABLE_NOT_EXISTS: "La table {variable} n'existe pas",
96
- NO_SCHEMA: "La table {variable} n'a pas de schéma",
97
- GROUP_UNIQUE: "Le groupe {variable} doit être unique, contenu dupliqué trouvé dans {variable}",
98
- FIELD_UNIQUE: "Le champ {variable} doit être unique, trouvé {variable} à la place",
99
- FIELD_REQUIRED: "Le champ {variable} est obligatoire",
100
- INVALID_ID: "Le(s) ID donné(s) n'est/ne sont pas valide(s)",
101
- INVALID_TYPE: "Attendu que {variable} soit de type {variable}, mais trouvé {variable} à la place",
102
- INVALID_PARAMETERS: "Les paramètres donnés ne sont pas valides",
103
- INVALID_REGEX_MATCH: "Le champ {variable} ne correspond pas au modèle attendu",
104
- NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
105
- ? "veuillez exécuter avec '--env-file=.env'"
106
- : "veuillez utiliser dotenv",
107
- },
108
- es: {
109
- TABLE_EMPTY: "La tabla {variable} está vacía",
110
- TABLE_EXISTS: "La tabla {variable} ya existe",
111
- TABLE_NOT_EXISTS: "La tabla {variable} no existe",
112
- NO_SCHEMA: "La tabla {variable} no tiene un esquema",
113
- GROUP_UNIQUE: "El grupo {variable} debe ser único, se encontró contenido duplicado en {variable}",
114
- FIELD_UNIQUE: "El campo {variable} debe ser único, se encontró {variable} en su lugar",
115
- FIELD_REQUIRED: "El campo {variable} es obligatorio",
116
- INVALID_ID: "El/los ID proporcionado(s) no es/son válido(s)",
117
- INVALID_TYPE: "Se espera que {variable} sea {variable}, pero se encontró {variable} en su lugar",
118
- INVALID_PARAMETERS: "Los parámetros proporcionados no son válidos",
119
- INVALID_REGEX_MATCH: "El campo {variable} no coincide con el patrón esperado",
120
- NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
121
- ? "por favor ejecute con '--env-file=.env'"
122
- : "por favor use dotenv",
123
- },
124
- };
125
61
  createError(name, variable) {
126
- const errorMessage = Inibase.errorMessages[this.language]?.[name];
127
- if (!errorMessage)
128
- return new Error("ERR");
129
- const error = new Error(variable
130
- ? Array.isArray(variable)
131
- ? errorMessage.replace(/\{variable\}/g, () => variable.shift()?.toString() ?? "")
132
- : errorMessage.replaceAll("{variable}", `'${variable.toString()}'`)
133
- : errorMessage.replaceAll("{variable}", ""));
134
- error.name = name;
135
- return error;
62
+ return Utils.createError(this.language, name, variable);
63
+ }
64
+ validateName(name) {
65
+ Utils.validateName(name, this.language);
66
+ }
67
+ validateSchema(schema) {
68
+ Utils.validateSchema(schema, this.language);
136
69
  }
137
70
  getFileExtension(tableName) {
138
71
  let mainExtension = this.fileExtension;
@@ -163,6 +96,9 @@ export default class Inibase {
163
96
  * @param {TableConfig} [config]
164
97
  */
165
98
  async createTable(tableName, schema, config) {
99
+ this.validateName(tableName);
100
+ if (schema)
101
+ this.validateSchema(schema);
166
102
  const tablePath = join(this.databasePath, tableName);
167
103
  if (await File.isExists(tablePath))
168
104
  throw this.createError("TABLE_EXISTS", tableName);
@@ -211,11 +147,15 @@ export default class Inibase {
211
147
  * @param {(TableConfig&{name?: string})} [config]
212
148
  */
213
149
  async updateTable(tableName, schema, config) {
150
+ this.validateName(tableName);
151
+ if (config?.name)
152
+ this.validateName(config.name);
214
153
  const table = await this.getTable(tableName);
215
154
  if (!table)
216
155
  return;
217
156
  const tablePath = join(this.databasePath, tableName);
218
157
  if (schema) {
158
+ this.validateSchema(schema);
219
159
  // remove id from schema
220
160
  schema = schema.filter(({ key }) => !["id", "createdAt", "updatedAt"].includes(key));
221
161
  let schemaIdFilePath = "";
@@ -330,6 +270,7 @@ export default class Inibase {
330
270
  * @return {*} {Promise<TableObject | undefined>}
331
271
  */
332
272
  async getTable(tableName) {
273
+ this.validateName(tableName);
333
274
  const tablePath = join(this.databasePath, tableName);
334
275
  if (!(await File.isExists(tablePath)))
335
276
  throw this.createError("TABLE_NOT_EXISTS", tableName);
@@ -349,6 +290,7 @@ export default class Inibase {
349
290
  return globalConfig[this.databasePath].tables?.get(tableName);
350
291
  }
351
292
  async getTableSchema(tableName) {
293
+ this.validateName(tableName);
352
294
  const tablePath = join(this.databasePath, tableName);
353
295
  let schemaFile;
354
296
  let schema;
@@ -1151,6 +1093,7 @@ export default class Inibase {
1151
1093
  * @param {string} tableName
1152
1094
  */
1153
1095
  async clearCache(tableName) {
1096
+ this.validateName(tableName);
1154
1097
  const cacheFolderPath = join(this.databasePath, tableName, ".cache");
1155
1098
  await rm(cacheFolderPath, { recursive: true, force: true });
1156
1099
  await mkdir(cacheFolderPath);
@@ -1159,12 +1102,15 @@ export default class Inibase {
1159
1102
  page: 1,
1160
1103
  perPage: 15,
1161
1104
  }, onlyOne, onlyLinesNumbers, _whereIsLinesNumbers) {
1105
+ this.validateName(tableName);
1162
1106
  const tablePath = join(this.databasePath, tableName);
1163
1107
  // Ensure options.columns is an array
1164
1108
  if (options.columns) {
1165
1109
  options.columns = Array.isArray(options.columns)
1166
1110
  ? options.columns
1167
1111
  : [options.columns];
1112
+ for (const column of options.columns)
1113
+ this.validateName(column);
1168
1114
  if (options.columns.length && !options.columns.includes("id"))
1169
1115
  options.columns.push("id");
1170
1116
  }
@@ -1407,6 +1353,12 @@ export default class Inibase {
1407
1353
  page: 1,
1408
1354
  perPage: 15,
1409
1355
  };
1356
+ this.validateName(tableName);
1357
+ if (options.columns)
1358
+ for (const column of (Array.isArray(options.columns)
1359
+ ? options.columns
1360
+ : [options.columns]))
1361
+ this.validateName(column);
1410
1362
  const tablePath = join(this.databasePath, tableName);
1411
1363
  await this.getTable(tableName);
1412
1364
  if (!globalConfig[this.databasePath].tables?.get(tableName)?.schema)
@@ -1487,6 +1439,12 @@ export default class Inibase {
1487
1439
  perPage: 15,
1488
1440
  }, returnUpdatedData, _whereIsLinesNumbers) {
1489
1441
  const renameList = [];
1442
+ this.validateName(tableName);
1443
+ if (options.columns)
1444
+ for (const column of (Array.isArray(options.columns)
1445
+ ? options.columns
1446
+ : [options.columns]))
1447
+ this.validateName(column);
1490
1448
  const tablePath = join(this.databasePath, tableName);
1491
1449
  await this.throwErrorIfTableEmpty(tableName);
1492
1450
  let clonedData = structuredClone(data);
@@ -1601,6 +1559,7 @@ export default class Inibase {
1601
1559
  * @return {boolean | null} {(Promise<boolean | null>)}
1602
1560
  */
1603
1561
  async delete(tableName, where, _whereIsLinesNumbers) {
1562
+ this.validateName(tableName);
1604
1563
  const tablePath = join(this.databasePath, tableName);
1605
1564
  await this.throwErrorIfTableEmpty(tableName);
1606
1565
  if (!where) {
@@ -1694,11 +1653,14 @@ export default class Inibase {
1694
1653
  return false;
1695
1654
  }
1696
1655
  async sum(tableName, columns, where) {
1697
- const RETURN = {};
1698
- const tablePath = join(this.databasePath, tableName);
1699
- await this.throwErrorIfTableEmpty(tableName);
1656
+ this.validateName(tableName);
1700
1657
  if (!Array.isArray(columns))
1701
1658
  columns = [columns];
1659
+ for (const column of columns)
1660
+ this.validateName(column);
1661
+ await this.throwErrorIfTableEmpty(tableName);
1662
+ const RETURN = {};
1663
+ const tablePath = join(this.databasePath, tableName);
1702
1664
  for await (const column of columns) {
1703
1665
  const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
1704
1666
  if (await File.isExists(columnPath)) {
@@ -1715,11 +1677,14 @@ export default class Inibase {
1715
1677
  return columns.length > 1 ? RETURN : Object.values(RETURN)[0];
1716
1678
  }
1717
1679
  async max(tableName, columns, where) {
1680
+ this.validateName(tableName);
1681
+ if (!Array.isArray(columns))
1682
+ columns = [columns];
1683
+ for (const column of columns)
1684
+ this.validateName(column);
1718
1685
  const RETURN = {};
1719
1686
  const tablePath = join(this.databasePath, tableName);
1720
1687
  await this.throwErrorIfTableEmpty(tableName);
1721
- if (!Array.isArray(columns))
1722
- columns = [columns];
1723
1688
  for await (const column of columns) {
1724
1689
  const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
1725
1690
  if (await File.isExists(columnPath)) {
@@ -1736,11 +1701,14 @@ export default class Inibase {
1736
1701
  return RETURN;
1737
1702
  }
1738
1703
  async min(tableName, columns, where) {
1704
+ this.validateName(tableName);
1705
+ if (!Array.isArray(columns))
1706
+ columns = [columns];
1707
+ for (const column of columns)
1708
+ this.validateName(column);
1739
1709
  const RETURN = {};
1740
1710
  const tablePath = join(this.databasePath, tableName);
1741
1711
  await this.throwErrorIfTableEmpty(tableName);
1742
- if (!Array.isArray(columns))
1743
- columns = [columns];
1744
1712
  for await (const column of columns) {
1745
1713
  const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
1746
1714
  if (await File.isExists(columnPath)) {
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ComparisonOperator, Field, FieldType, Schema } from "./index.js";
1
+ import type { ComparisonOperator, ErrorCode, ErrorLang, Field, FieldType, Schema } from "./index.js";
2
2
  /**
3
3
  * Type guard function to check if the input is an array of objects.
4
4
  *
@@ -218,3 +218,50 @@ 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
+ * Translated error messages for every supported language and error code.
223
+ * The `{variable}` placeholder is replaced with the relevant value by
224
+ * {@link createError}.
225
+ */
226
+ export declare const ERROR_MESSAGES: Record<ErrorLang, Record<ErrorCode, string>>;
227
+ /**
228
+ * Creates a translated error exactly like the ones thrown by Inibase methods.
229
+ *
230
+ * @param language - The language to render the error message in.
231
+ * @param name - The error code, also used as the error `name`.
232
+ * @param variable - Optional value(s) used to fill the `{variable}` placeholders.
233
+ * @returns An `Error` whose `name` is the error code and `message` is translated.
234
+ */
235
+ export declare const createError: (language: ErrorLang, name: ErrorCode, variable?: string | number | (string | number)[]) => Error;
236
+ /**
237
+ * Validates that a string is a safe name for a table, database or column.
238
+ *
239
+ * Names must start with an alphanumeric character (Latin or Arabic), followed
240
+ * by alphanumerics (Latin or Arabic), underscores, hyphens or spaces, and be
241
+ * at most 255 characters long. Leading or trailing spaces are not allowed.
242
+ *
243
+ * This prevents path traversal (`../`, `..\\`), null-byte injection (`\0`),
244
+ * shell injection (`;`, `|`, backticks, `$()`, tabs, newlines, slashes) and
245
+ * other reserved-name issues.
246
+ *
247
+ * @param input - The value to be checked.
248
+ * @returns boolean - True if the name is safe to use, false otherwise.
249
+ */
250
+ export declare const isValidName: (input: unknown) => input is string;
251
+ /**
252
+ * Validates that a string is a safe name for a table, database or column and
253
+ * throws a translated `INVALID_NAME` error if it is not.
254
+ *
255
+ * @param name - The name to validate.
256
+ * @param language - The language to render the error message in.
257
+ * @throws {Error} If the name is not a safe name.
258
+ */
259
+ export declare const validateName: (name: string, language?: ErrorLang) => void;
260
+ /**
261
+ * Recursively validates every field key of a schema.
262
+ *
263
+ * @param schema - The schema to validate.
264
+ * @param language - The language to render the error message in.
265
+ * @throws {Error} If any field key is not a safe name.
266
+ */
267
+ export declare const validateSchema: (schema: Schema, language?: ErrorLang) => void;
package/dist/utils.js CHANGED
@@ -628,3 +628,150 @@ export function addIdToSchema(schema, startWithID) {
628
628
  const addIdToSchemaHelper = (schema) => schema.map(addIdToField);
629
629
  return addIdToSchemaHelper(clonedSchema);
630
630
  }
631
+ /**
632
+ * Translated error messages for every supported language and error code.
633
+ * The `{variable}` placeholder is replaced with the relevant value by
634
+ * {@link createError}.
635
+ */
636
+ export const ERROR_MESSAGES = {
637
+ en: {
638
+ TABLE_EMPTY: "Table {variable} is empty",
639
+ TABLE_EXISTS: "Table {variable} already exists",
640
+ TABLE_NOT_EXISTS: "Table {variable} doesn't exist",
641
+ NO_SCHEMA: "Table {variable} does't have a schema",
642
+ GROUP_UNIQUE: "Group {variable} should be unique, got duplicated content in {variable}",
643
+ FIELD_UNIQUE: "Field {variable} should be unique, got {variable} instead",
644
+ FIELD_REQUIRED: "Field {variable} is required",
645
+ INVALID_ID: "The given ID(s) is/are not valid(s)",
646
+ INVALID_TYPE: "Expect {variable} to be {variable}, got {variable} instead",
647
+ INVALID_PARAMETERS: "The given parameters are not valid",
648
+ INVALID_REGEX_MATCH: "Field {variable} does not match the expected pattern",
649
+ INVALID_NAME: "Name {variable} is not valid",
650
+ NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
651
+ ? "please run with '--env-file=.env'"
652
+ : "please use dotenv",
653
+ },
654
+ ar: {
655
+ TABLE_EMPTY: "الجدول {variable} فارغ",
656
+ TABLE_EXISTS: "الجدول {variable} موجود بالفعل",
657
+ TABLE_NOT_EXISTS: "الجدول {variable} غير موجود",
658
+ NO_SCHEMA: "الجدول {variable} ليس لديه مخطط",
659
+ GROUP_UNIQUE: "المجموعة {variable} يجب أن تكون فريدة، تم العثور على محتوى مكرر في {variable}",
660
+ FIELD_UNIQUE: "الحقل {variable} يجب أن يكون فريدًا، تم العثور على {variable} بدلاً من ذلك",
661
+ FIELD_REQUIRED: "الحقل {variable} مطلوب",
662
+ INVALID_ID: "المعرف أو المعرفات المقدمة غير صالحة",
663
+ INVALID_TYPE: "من المتوقع أن يكون {variable} من النوع {variable}، لكن تم العثور على {variable} بدلاً من ذلك",
664
+ INVALID_PARAMETERS: "المعلمات المقدمة غير صالحة",
665
+ INVALID_REGEX_MATCH: "الحقل {variable} لا يتطابق مع النمط المتوقع",
666
+ INVALID_NAME: "الاسم {variable} غير صالح",
667
+ NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
668
+ ? "يرجى التشغيل باستخدام '--env-file=.env'"
669
+ : "يرجى استخدام dotenv",
670
+ },
671
+ fr: {
672
+ TABLE_EMPTY: "La table {variable} est vide",
673
+ TABLE_EXISTS: "La table {variable} existe déjà",
674
+ TABLE_NOT_EXISTS: "La table {variable} n'existe pas",
675
+ NO_SCHEMA: "La table {variable} n'a pas de schéma",
676
+ GROUP_UNIQUE: "Le groupe {variable} doit être unique, contenu dupliqué trouvé dans {variable}",
677
+ FIELD_UNIQUE: "Le champ {variable} doit être unique, trouvé {variable} à la place",
678
+ FIELD_REQUIRED: "Le champ {variable} est obligatoire",
679
+ INVALID_ID: "Le(s) ID donné(s) n'est/ne sont pas valide(s)",
680
+ INVALID_TYPE: "Attendu que {variable} soit de type {variable}, mais trouvé {variable} à la place",
681
+ INVALID_PARAMETERS: "Les paramètres donnés ne sont pas valides",
682
+ INVALID_REGEX_MATCH: "Le champ {variable} ne correspond pas au modèle attendu",
683
+ INVALID_NAME: "Le nom {variable} n'est pas valide",
684
+ NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
685
+ ? "veuillez exécuter avec '--env-file=.env'"
686
+ : "veuillez utiliser dotenv",
687
+ },
688
+ es: {
689
+ TABLE_EMPTY: "La tabla {variable} está vacía",
690
+ TABLE_EXISTS: "La tabla {variable} ya existe",
691
+ TABLE_NOT_EXISTS: "La tabla {variable} no existe",
692
+ NO_SCHEMA: "La tabla {variable} no tiene un esquema",
693
+ GROUP_UNIQUE: "El grupo {variable} debe ser único, se encontró contenido duplicado en {variable}",
694
+ FIELD_UNIQUE: "El campo {variable} debe ser único, se encontró {variable} en su lugar",
695
+ FIELD_REQUIRED: "El campo {variable} es obligatorio",
696
+ INVALID_ID: "El/los ID proporcionado(s) no es/son válido(s)",
697
+ INVALID_TYPE: "Se espera que {variable} sea {variable}, pero se encontró {variable} en su lugar",
698
+ INVALID_PARAMETERS: "Los parámetros proporcionados no son válidos",
699
+ INVALID_REGEX_MATCH: "El campo {variable} no coincide con el patrón esperado",
700
+ INVALID_NAME: "El nombre {variable} no es válido",
701
+ NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
702
+ ? "por favor ejecute con '--env-file=.env'"
703
+ : "por favor use dotenv",
704
+ },
705
+ };
706
+ /**
707
+ * Creates a translated error exactly like the ones thrown by Inibase methods.
708
+ *
709
+ * @param language - The language to render the error message in.
710
+ * @param name - The error code, also used as the error `name`.
711
+ * @param variable - Optional value(s) used to fill the `{variable}` placeholders.
712
+ * @returns An `Error` whose `name` is the error code and `message` is translated.
713
+ */
714
+ export const createError = (language, name, variable) => {
715
+ const errorMessage = ERROR_MESSAGES[language]?.[name];
716
+ if (!errorMessage)
717
+ return new Error("ERR");
718
+ const error = new Error(variable
719
+ ? Array.isArray(variable)
720
+ ? errorMessage.replace(/\{variable\}/g, () => variable.shift()?.toString() ?? "")
721
+ : errorMessage.replaceAll("{variable}", `'${variable.toString()}'`)
722
+ : errorMessage.replaceAll("{variable}", ""));
723
+ error.name = name;
724
+ return error;
725
+ };
726
+ /**
727
+ * Validates that a string is a safe name for a table, database or column.
728
+ *
729
+ * Names must start with an alphanumeric character (Latin or Arabic), followed
730
+ * by alphanumerics (Latin or Arabic), underscores, hyphens or spaces, and be
731
+ * at most 255 characters long. Leading or trailing spaces are not allowed.
732
+ *
733
+ * This prevents path traversal (`../`, `..\\`), null-byte injection (`\0`),
734
+ * shell injection (`;`, `|`, backticks, `$()`, tabs, newlines, slashes) and
735
+ * other reserved-name issues.
736
+ *
737
+ * @param input - The value to be checked.
738
+ * @returns boolean - True if the name is safe to use, false otherwise.
739
+ */
740
+ export const isValidName = (input) => typeof input === "string" &&
741
+ input.length > 0 &&
742
+ input.length <= 255 &&
743
+ validNamePattern.test(input);
744
+ // Word characters: Latin alphanumerics, underscores, hyphens and Arabic blocks.
745
+ const nameWordCharClass = "a-zA-Z0-9_\\u0600-\\u06FF\\u0750-\\u077F\\u08A0-\\u08FF-";
746
+ // Leading characters: same as word chars minus underscore and hyphen.
747
+ const nameFirstCharClass = "a-zA-Z0-9\\u0600-\\u06FF\\u0750-\\u077F\\u08A0-\\u08FF";
748
+ // eslint-disable-next-line no-misleading-character-class
749
+ const validNamePattern = new RegExp(`^[${nameFirstCharClass}](?:[${nameWordCharClass} ]*[${nameWordCharClass}])?$`);
750
+ /**
751
+ * Validates that a string is a safe name for a table, database or column and
752
+ * throws a translated `INVALID_NAME` error if it is not.
753
+ *
754
+ * @param name - The name to validate.
755
+ * @param language - The language to render the error message in.
756
+ * @throws {Error} If the name is not a safe name.
757
+ */
758
+ export const validateName = (name, language = "en") => {
759
+ if (!isValidName(name))
760
+ throw createError(language, "INVALID_NAME", name);
761
+ };
762
+ /**
763
+ * Recursively validates every field key of a schema.
764
+ *
765
+ * @param schema - The schema to validate.
766
+ * @param language - The language to render the error message in.
767
+ * @throws {Error} If any field key is not a safe name.
768
+ */
769
+ export const validateSchema = (schema, language = "en") => {
770
+ for (const field of schema) {
771
+ validateName(field.key, language);
772
+ if (field.table)
773
+ validateName(field.table, language);
774
+ if (field.children && isArrayOfObjects(field.children))
775
+ validateSchema(field.children, language);
776
+ }
777
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inibase",
3
- "version": "1.5.11",
3
+ "version": "1.6.1",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Karim Amahtil",