inibase 1.1.4 → 1.1.6

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/README.md CHANGED
@@ -26,6 +26,7 @@
26
26
 
27
27
  ```js
28
28
  import Inibase from "inibase";
29
+ // const db = new Inibase("databaseName", ".", "es");
29
30
  const db = new Inibase("databaseName");
30
31
 
31
32
  // Get all items from "user" table
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import "dotenv/config";
2
2
  export interface Data {
3
- id?: number | string;
3
+ id?: string;
4
4
  [key: string]: any;
5
5
  createdAt?: number;
6
6
  updatedAt?: number;
@@ -50,19 +50,20 @@ declare global {
50
50
  entries<T extends object>(o: T): Entries<T>;
51
51
  }
52
52
  }
53
- export type ErrorCodes = "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 type ErrorLang = "en";
53
+ export type ErrorCodes = "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 type ErrorLang = "en" | "ar" | "fr" | "es";
55
55
  export default class Inibase {
56
56
  pageInfo: Record<string, pageInfo>;
57
+ language: ErrorLang;
57
58
  salt: Buffer;
58
59
  private databasePath;
59
60
  private fileExtension;
60
61
  private tablesMap;
61
62
  private uniqueMap;
62
63
  private totalItems;
63
- constructor(database: string, mainFolder?: string);
64
+ constructor(database: string, mainFolder?: string, language?: ErrorLang);
64
65
  private static errorMessages;
65
- createError(name: ErrorCodes, variable?: string | number | (string | number)[], language?: ErrorLang): Error;
66
+ createError(name: ErrorCodes, variable?: string | number | (string | number)[]): Error;
66
67
  clear(): void;
67
68
  private getFileExtension;
68
69
  private _schemaToIdsPath;
package/dist/index.js CHANGED
@@ -12,15 +12,17 @@ import * as UtilsServer from "./utils.server.js";
12
12
  process.removeAllListeners("warning");
13
13
  export default class Inibase {
14
14
  pageInfo;
15
+ language;
15
16
  salt;
16
17
  databasePath;
17
18
  fileExtension = ".txt";
18
19
  tablesMap;
19
20
  uniqueMap;
20
21
  totalItems;
21
- constructor(database, mainFolder = ".") {
22
+ constructor(database, mainFolder = ".", language = "en") {
22
23
  this.databasePath = join(mainFolder, database);
23
24
  this.clear();
25
+ this.language = language;
24
26
  if (!process.env.INIBASE_SECRET) {
25
27
  if (existsSync(".env") &&
26
28
  readFileSync(".env").includes("INIBASE_SECRET="))
@@ -37,6 +39,7 @@ export default class Inibase {
37
39
  TABLE_EXISTS: "Table {variable} already exists",
38
40
  TABLE_NOT_EXISTS: "Table {variable} doesn't exist",
39
41
  NO_SCHEMA: "Table {variable} does't have a schema",
42
+ GROUP_UNIQUE: "Group {variable} should be unique, got duplicated content in {variable}",
40
43
  FIELD_UNIQUE: "Field {variable} should be unique, got {variable} instead",
41
44
  FIELD_REQUIRED: "Field {variable} is required",
42
45
  INVALID_ID: "The given ID(s) is/are not valid(s)",
@@ -47,9 +50,57 @@ export default class Inibase {
47
50
  ? "please run with '--env-file=.env'"
48
51
  : "please use dotenv",
49
52
  },
53
+ ar: {
54
+ TABLE_EMPTY: "الجدول {variable} فارغ",
55
+ TABLE_EXISTS: "الجدول {variable} موجود بالفعل",
56
+ TABLE_NOT_EXISTS: "الجدول {variable} غير موجود",
57
+ NO_SCHEMA: "الجدول {variable} ليس لديه مخطط",
58
+ GROUP_UNIQUE: "المجموعة {variable} يجب أن تكون فريدة، تم العثور على محتوى مكرر في {variable}",
59
+ FIELD_UNIQUE: "الحقل {variable} يجب أن يكون فريدًا، تم العثور على {variable} بدلاً من ذلك",
60
+ FIELD_REQUIRED: "الحقل {variable} مطلوب",
61
+ INVALID_ID: "المعرف أو المعرفات المقدمة غير صالحة",
62
+ INVALID_TYPE: "من المتوقع أن يكون {variable} من النوع {variable}، لكن تم العثور على {variable} بدلاً من ذلك",
63
+ INVALID_PARAMETERS: "المعلمات المقدمة غير صالحة",
64
+ INVALID_REGEX_MATCH: "الحقل {variable} لا يتطابق مع النمط المتوقع",
65
+ NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
66
+ ? "يرجى التشغيل باستخدام '--env-file=.env'"
67
+ : "يرجى استخدام dotenv",
68
+ },
69
+ fr: {
70
+ TABLE_EMPTY: "La table {variable} est vide",
71
+ TABLE_EXISTS: "La table {variable} existe déjà",
72
+ TABLE_NOT_EXISTS: "La table {variable} n'existe pas",
73
+ NO_SCHEMA: "La table {variable} n'a pas de schéma",
74
+ GROUP_UNIQUE: "Le groupe {variable} doit être unique, contenu dupliqué trouvé dans {variable}",
75
+ FIELD_UNIQUE: "Le champ {variable} doit être unique, trouvé {variable} à la place",
76
+ FIELD_REQUIRED: "Le champ {variable} est obligatoire",
77
+ INVALID_ID: "Le(s) ID donné(s) n'est/ne sont pas valide(s)",
78
+ INVALID_TYPE: "Attendu que {variable} soit de type {variable}, mais trouvé {variable} à la place",
79
+ INVALID_PARAMETERS: "Les paramètres donnés ne sont pas valides",
80
+ INVALID_REGEX_MATCH: "Le champ {variable} ne correspond pas au modèle attendu",
81
+ NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
82
+ ? "veuillez exécuter avec '--env-file=.env'"
83
+ : "veuillez utiliser dotenv",
84
+ },
85
+ es: {
86
+ TABLE_EMPTY: "La tabla {variable} está vacía",
87
+ TABLE_EXISTS: "La tabla {variable} ya existe",
88
+ TABLE_NOT_EXISTS: "La tabla {variable} no existe",
89
+ NO_SCHEMA: "La tabla {variable} no tiene un esquema",
90
+ GROUP_UNIQUE: "El grupo {variable} debe ser único, se encontró contenido duplicado en {variable}",
91
+ FIELD_UNIQUE: "El campo {variable} debe ser único, se encontró {variable} en su lugar",
92
+ FIELD_REQUIRED: "El campo {variable} es obligatorio",
93
+ INVALID_ID: "El/los ID proporcionado(s) no es/son válido(s)",
94
+ INVALID_TYPE: "Se espera que {variable} sea {variable}, pero se encontró {variable} en su lugar",
95
+ INVALID_PARAMETERS: "Los parámetros proporcionados no son válidos",
96
+ INVALID_REGEX_MATCH: "El campo {variable} no coincide con el patrón esperado",
97
+ NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
98
+ ? "por favor ejecute con '--env-file=.env'"
99
+ : "por favor use dotenv",
100
+ },
50
101
  };
51
- createError(name, variable, language = "en") {
52
- const errorMessage = Inibase.errorMessages[language]?.[name];
102
+ createError(name, variable) {
103
+ const errorMessage = Inibase.errorMessages[this.language]?.[name];
53
104
  if (!errorMessage)
54
105
  return new Error("ERR");
55
106
  const error = new Error(variable
@@ -332,6 +383,10 @@ export default class Inibase {
332
383
  Utils.isArrayOfObjects(field.children))
333
384
  this._validateData(data[field.key], field.children, skipRequiredField);
334
385
  else {
386
+ if (field.table &&
387
+ Utils.isObject(data[field.key]) &&
388
+ Object.hasOwn(data[field.key], "id"))
389
+ data[field.key] = data[field.key].id;
335
390
  if (field.regex) {
336
391
  const regex = UtilsServer.getCachedRegex(field.regex);
337
392
  if (!regex.test(data[field.key]))
@@ -366,8 +421,9 @@ export default class Inibase {
366
421
  }
367
422
  }
368
423
  async validateData(tableName, data, skipRequiredField = false) {
424
+ const clonedData = structuredClone(data);
369
425
  // Skip ID and (created|updated)At
370
- this._validateData(data, this.tablesMap.get(tableName).schema.slice(1, -2), skipRequiredField);
426
+ this._validateData(clonedData, this.tablesMap.get(tableName).schema.slice(1, -2), skipRequiredField);
371
427
  await this.checkUnique(tableName);
372
428
  }
373
429
  cleanObject(obj) {
@@ -455,16 +511,24 @@ export default class Inibase {
455
511
  let index = 0;
456
512
  let shouldContinueParent = false; // Flag to manage parent loop continuation
457
513
  const mergedLineNumbers = new Set();
514
+ const fieldsKeys = [];
458
515
  for await (const [columnID, values] of valueObject.columnsValues) {
459
516
  index++;
460
517
  const field = flattenSchema.find(({ id }) => id === columnID);
518
+ fieldsKeys.push(field.key);
461
519
  const [_, 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);
462
520
  if (totalLines > 0) {
463
521
  if (valueObject.columnsValues.size === 1 ||
464
- hasDuplicates(lineNumbers, mergedLineNumbers)) {
522
+ (valueObject.columnsValues.size === index &&
523
+ hasDuplicates(lineNumbers, mergedLineNumbers))) {
465
524
  this.uniqueMap = new Map();
525
+ if (valueObject.columnsValues.size > 1)
526
+ throw this.createError("GROUP_UNIQUE", [
527
+ fieldsKeys.join(" & "),
528
+ field.key,
529
+ ]);
466
530
  throw this.createError("FIELD_UNIQUE", [
467
- field.key,
531
+ fieldsKeys.join(" & "),
468
532
  Array.from(values).join(", "),
469
533
  ]);
470
534
  }
package/dist/utils.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { ComparisonOperator, Field, FieldType, Schema } from "./index.js";
7
7
  *
8
8
  * Note: Considers empty arrays and arrays where every element is an object.
9
9
  */
10
- export declare const isArrayOfObjects: (input: any) => input is Record<any, any>[];
10
+ export declare const isArrayOfObjects: (input: unknown) => input is Record<any, any>[];
11
11
  /**
12
12
  * Type guard function to check if the input is an array of arrays.
13
13
  *
@@ -16,7 +16,7 @@ export declare const isArrayOfObjects: (input: any) => input is Record<any, any>
16
16
  *
17
17
  * Note: Considers empty arrays and arrays where every element is also an array.
18
18
  */
19
- export declare const isArrayOfArrays: (input: any) => input is any[][];
19
+ export declare const isArrayOfArrays: (input: unknown) => input is any[][];
20
20
  /**
21
21
  * Type guard function to check if the input is an array of nulls or an array of arrays of nulls.
22
22
  *
@@ -25,7 +25,7 @@ export declare const isArrayOfArrays: (input: any) => input is any[][];
25
25
  *
26
26
  * Note: Recursively checks each element, allowing for nested arrays of nulls.
27
27
  */
28
- export declare const isArrayOfNulls: (input: any) => input is null[] | null[][];
28
+ export declare const isArrayOfNulls: (input: unknown) => input is null[] | null[][];
29
29
  /**
30
30
  * Type guard function to check if the input is an object.
31
31
  *
@@ -34,17 +34,7 @@ export declare const isArrayOfNulls: (input: any) => input is null[] | null[][];
34
34
  *
35
35
  * Note: Checks if the input is non-null and either has 'Object' as its constructor name or is of type 'object' without being an array.
36
36
  */
37
- export declare const isObject: (obj: any) => obj is Record<any, any>;
38
- /**
39
- * Recursively merges properties from a source object into a target object. If a property exists in both, the source's value overwrites the target's.
40
- *
41
- * @param target - The target object to merge properties into.
42
- * @param source - The source object from which properties are merged.
43
- * @returns any - The modified target object with merged properties.
44
- *
45
- * Note: Performs a deep merge for nested objects. Non-object properties are directly overwritten.
46
- */
47
- export declare const deepMerge: (target: any, source: any) => any;
37
+ export declare const isObject: (object: unknown) => object is Record<any, any>;
48
38
  /**
49
39
  * Type guard function to check if the input is a number.
50
40
  *
@@ -53,7 +43,7 @@ export declare const deepMerge: (target: any, source: any) => any;
53
43
  *
54
44
  * Note: Validates that the input can be parsed as a float and that subtracting zero results in a number, ensuring it's a numeric value.
55
45
  */
56
- export declare const isNumber: (input: any) => input is number;
46
+ export declare const isNumber: (input: unknown) => input is number;
57
47
  /**
58
48
  * Checks if the input is a valid email format.
59
49
  *
@@ -62,7 +52,7 @@ export declare const isNumber: (input: any) => input is number;
62
52
  *
63
53
  * Note: Uses a regular expression to validate the email format, ensuring it has parts separated by '@' and contains a domain with a period.
64
54
  */
65
- export declare const isEmail: (input: any) => boolean;
55
+ export declare const isEmail: (input: unknown) => boolean;
66
56
  /**
67
57
  * Checks if the input is a valid URL format.
68
58
  *
@@ -72,7 +62,7 @@ export declare const isEmail: (input: any) => boolean;
72
62
  * Note: Validates URLs including protocols (http/https), domain names, IP addresses, ports, paths, query strings, and fragments.
73
63
  * Also recognizes 'tel:' and 'mailto:' as valid URL formats, as well as strings starting with '#' without spaces.
74
64
  */
75
- export declare const isURL: (input: any) => boolean;
65
+ export declare const isURL: (input: unknown) => boolean;
76
66
  /**
77
67
  * Checks if the input contains HTML tags or entities.
78
68
  *
@@ -82,7 +72,7 @@ export declare const isURL: (input: any) => boolean;
82
72
  * Note: Uses a regular expression to detect HTML tags (like <tag>) and entities (like &entity;).
83
73
  * Recognizes both opening and closing tags, as well as self-closing tags.
84
74
  */
85
- export declare const isHTML: (input: any) => boolean;
75
+ export declare const isHTML: (input: unknown) => boolean;
86
76
  /**
87
77
  * Type guard function to check if the input is a string, excluding strings that match specific formats (number, boolean, email, URL, IP).
88
78
  *
@@ -91,7 +81,7 @@ export declare const isHTML: (input: any) => boolean;
91
81
  *
92
82
  * Note: Validates the input against being a number, boolean, email, URL, or IP address to ensure it's a general string.
93
83
  */
94
- export declare const isString: (input: any) => input is string;
84
+ export declare const isString: (input: unknown) => input is string;
95
85
  /**
96
86
  * Checks if the input is a valid IP address format.
97
87
  *
@@ -100,7 +90,7 @@ export declare const isString: (input: any) => input is string;
100
90
  *
101
91
  * Note: Uses a regular expression to validate IP addresses, ensuring they consist of four octets, each ranging from 0 to 255.
102
92
  */
103
- export declare const isIP: (input: any) => boolean;
93
+ export declare const isIP: (input: unknown) => input is string;
104
94
  /**
105
95
  * Type guard function to check if the input is a boolean or a string representation of a boolean.
106
96
  *
@@ -109,7 +99,7 @@ export declare const isIP: (input: any) => boolean;
109
99
  *
110
100
  * Note: Recognizes both boolean literals (true, false) and their string representations ("true", "false").
111
101
  */
112
- export declare const isBoolean: (input: any) => input is boolean;
102
+ export declare const isBoolean: (input: unknown) => input is boolean;
113
103
  /**
114
104
  * Type guard function to check if the input is a password based on a specific length criterion.
115
105
  *
@@ -118,28 +108,38 @@ export declare const isBoolean: (input: any) => input is boolean;
118
108
  *
119
109
  * Note: Specifically checks for string length to determine if it matches the defined password length criterion.
120
110
  */
121
- export declare const isPassword: (input: any) => input is string;
111
+ export declare const isPassword: (input: unknown) => input is string;
122
112
  /**
123
113
  * Checks if the input can be converted to a valid date.
124
114
  *
125
115
  * @param input - The input to be checked, can be of any type.
126
116
  * @returns A boolean indicating whether the input is a valid date.
127
117
  */
128
- export declare function isDate(input?: any): boolean;
118
+ export declare const isDate: (input: unknown) => input is Date | number;
129
119
  /**
130
120
  * Checks if the input is a valid ID.
131
121
  *
132
122
  * @param input - The input to be checked, can be of any type.
133
123
  * @returns A boolean indicating whether the input is a string representing a valid ID of length 32.
134
124
  */
135
- export declare const isValidID: (input: any) => input is string;
125
+ export declare const isValidID: (input: unknown) => input is string;
136
126
  /**
137
127
  * Checks if a given string is a valid JSON.
138
128
  *
139
- * @param {string} str - The string to be checked.
129
+ * @param {string} input - The string to be checked.
140
130
  * @returns {boolean} Returns true if the string is valid JSON, otherwise false.
141
131
  */
142
- export declare const isStringified: (str: string) => boolean;
132
+ export declare const isStringified: (input: unknown) => boolean;
133
+ /**
134
+ * Recursively merges properties from a source object into a target object. If a property exists in both, the source's value overwrites the target's.
135
+ *
136
+ * @param target - The target object to merge properties into.
137
+ * @param source - The source object from which properties are merged.
138
+ * @returns any - The modified target object with merged properties.
139
+ *
140
+ * Note: Performs a deep merge for nested objects. Non-object properties are directly overwritten.
141
+ */
142
+ export declare const deepMerge: (target: any, source: any) => any;
143
143
  /**
144
144
  * Identifies and returns properties that have changed between two objects.
145
145
  *
@@ -168,10 +168,7 @@ export declare const filterSchema: (schema: Schema, callback: (arg0: Field) => b
168
168
  * @returns A boolean indicating whether the value matches the specified field type(s).
169
169
  */
170
170
  export declare const validateFieldType: (value: any, fieldType: FieldType | FieldType[], fieldChildrenType?: FieldType | FieldType[]) => boolean;
171
- export declare function FormatObjectCriteriaValue(value: string): [
172
- ComparisonOperator,
173
- string | number | boolean | null | (string | number | null)[]
174
- ];
171
+ export declare const FormatObjectCriteriaValue: (value: string) => [ComparisonOperator, string | number | boolean | null | (string | number | null)[]];
175
172
  /**
176
173
  * Get field from schema
177
174
  *
@@ -179,7 +176,7 @@ export declare function FormatObjectCriteriaValue(value: string): [
179
176
  * @param {string} keyPath Support dot notation path
180
177
  * @param {Schema} schema
181
178
  */
182
- export declare function getField(keyPath: string, schema: Schema): Field;
179
+ export declare const getField: (keyPath: string, schema: Schema) => Field;
183
180
  /**
184
181
  * Override a schema field, key, type or other properties
185
182
  *
@@ -191,10 +188,10 @@ export declare function getField(keyPath: string, schema: Schema): Field;
191
188
  * type?: FieldType | FieldType[];
192
189
  * })} field
193
190
  */
194
- export declare function setField(keyPath: string, schema: Schema, field: Omit<Field, "key" | "type"> & {
191
+ export declare const setField: (keyPath: string, schema: Schema, field: Omit<Field, "key" | "type"> & {
195
192
  key?: string;
196
193
  type?: FieldType | FieldType[];
197
- }): Field;
194
+ }) => Field;
198
195
  /**
199
196
  * Remove field from schema
200
197
  *
@@ -202,4 +199,4 @@ export declare function setField(keyPath: string, schema: Schema, field: Omit<Fi
202
199
  * @param {string} keyPath Support dot notation path
203
200
  * @param {Schema} schema
204
201
  */
205
- export declare function unsetField(keyPath: string, schema: Schema): Field;
202
+ export declare const unsetField: (keyPath: string, schema: Schema) => Field;
package/dist/utils.js CHANGED
@@ -15,7 +15,7 @@ export const isArrayOfObjects = (input) => Array.isArray(input) && (input.length
15
15
  *
16
16
  * Note: Considers empty arrays and arrays where every element is also an array.
17
17
  */
18
- export const isArrayOfArrays = (input) => Array.isArray(input) && (input.length === 0 || input.every(Array.isArray));
18
+ export const isArrayOfArrays = (input) => Array.isArray(input) && input.length > 0 && input.every(Array.isArray);
19
19
  /**
20
20
  * Type guard function to check if the input is an array of nulls or an array of arrays of nulls.
21
21
  *
@@ -24,7 +24,8 @@ export const isArrayOfArrays = (input) => Array.isArray(input) && (input.length
24
24
  *
25
25
  * Note: Recursively checks each element, allowing for nested arrays of nulls.
26
26
  */
27
- export const isArrayOfNulls = (input) => input.every((_input) => Array.isArray(_input) ? isArrayOfNulls(_input) : _input === null);
27
+ export const isArrayOfNulls = (input) => Array.isArray(input) &&
28
+ input.every((_input) => Array.isArray(_input) ? isArrayOfNulls(_input) : _input === null);
28
29
  /**
29
30
  * Type guard function to check if the input is an object.
30
31
  *
@@ -33,29 +34,9 @@ export const isArrayOfNulls = (input) => input.every((_input) => Array.isArray(_
33
34
  *
34
35
  * Note: Checks if the input is non-null and either has 'Object' as its constructor name or is of type 'object' without being an array.
35
36
  */
36
- export const isObject = (obj) => obj != null &&
37
- ((typeof obj === "object" && !Array.isArray(obj)) ||
38
- obj.constructor?.name === "Object");
39
- /**
40
- * Recursively merges properties from a source object into a target object. If a property exists in both, the source's value overwrites the target's.
41
- *
42
- * @param target - The target object to merge properties into.
43
- * @param source - The source object from which properties are merged.
44
- * @returns any - The modified target object with merged properties.
45
- *
46
- * Note: Performs a deep merge for nested objects. Non-object properties are directly overwritten.
47
- */
48
- export const deepMerge = (target, source) => {
49
- for (const key in source) {
50
- if (Object.hasOwn(source, key)) {
51
- if (isObject(source[key]) && isObject(target[key]))
52
- target[key] = deepMerge(target[key], source[key]);
53
- else if (source[key] !== null)
54
- target[key] = source[key];
55
- }
56
- }
57
- return target;
58
- };
37
+ export const isObject = (object) => object != null &&
38
+ ((typeof object === "object" && !Array.isArray(object)) ||
39
+ object.constructor?.name === "Object");
59
40
  /**
60
41
  * Type guard function to check if the input is a number.
61
42
  *
@@ -64,7 +45,24 @@ export const deepMerge = (target, source) => {
64
45
  *
65
46
  * Note: Validates that the input can be parsed as a float and that subtracting zero results in a number, ensuring it's a numeric value.
66
47
  */
67
- export const isNumber = (input) => !Number.isNaN(Number.parseFloat(input)) && !Number.isNaN(input - 0);
48
+ export const isNumber = (input) => {
49
+ // Case 1: It's already a number (and not NaN/Infinity).
50
+ if (typeof input === "number")
51
+ return !Number.isNaN(input) && Number.isFinite(input);
52
+ // Case 2: It's a string that can parse to a finite number.
53
+ if (typeof input === "string") {
54
+ const trimmed = input.trim();
55
+ // Empty string or whitespace-only => not numeric
56
+ if (!trimmed)
57
+ return false;
58
+ const parsed = Number(trimmed); // or parseFloat(trimmed)
59
+ return !Number.isNaN(parsed) && Number.isFinite(parsed);
60
+ }
61
+ // Otherwise, not a numeric string or number
62
+ return false;
63
+ };
64
+ // As a literal (no double-escaping).
65
+ const emailPattern = /^[A-Za-z0-9!#%&'*+\/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#%&'*+\/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
68
66
  /**
69
67
  * Checks if the input is a valid email format.
70
68
  *
@@ -73,7 +71,22 @@ export const isNumber = (input) => !Number.isNaN(Number.parseFloat(input)) && !N
73
71
  *
74
72
  * Note: Uses a regular expression to validate the email format, ensuring it has parts separated by '@' and contains a domain with a period.
75
73
  */
76
- export const isEmail = (input) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(input));
74
+ export const isEmail = (input) => typeof input === "string" && emailPattern.test(String(input));
75
+ const urlPattern = new RegExp("^" +
76
+ // Optional protocol
77
+ "(https?:\\/\\/)?" +
78
+ // domain name (with underscore allowed), localhost, or ipv4
79
+ "((([a-z\\d_]([a-z\\d_\\-]*[a-z\\d_])*)\\.)+[a-z]{2,}|" +
80
+ "localhost|" +
81
+ "((\\d{1,3}\\.){3}\\d{1,3}))" +
82
+ // optional port
83
+ "(\\:\\d+)?" +
84
+ // path
85
+ "(\\/[-a-z\\d%_.~+]*)*" +
86
+ // query string
87
+ "(\\?[;&a-z\\d%_.~+=-]*)?" +
88
+ // fragment
89
+ "(\\#[-a-z\\d_]*)?$", "i");
77
90
  /**
78
91
  * Checks if the input is a valid URL format.
79
92
  *
@@ -88,17 +101,12 @@ export const isURL = (input) => {
88
101
  return false;
89
102
  if ((input[0] === "#" && !input.includes(" ")) ||
90
103
  input.startsWith("tel:") ||
91
- input.startsWith("mailto:"))
104
+ input.startsWith("mailto:") ||
105
+ URL.canParse(input))
92
106
  return true;
93
- const pattern = new RegExp("^(https?:\\/\\/)?" + // protocol
94
- "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
95
- "localhost|" + // OR localhost
96
- "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
97
- "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
98
- "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
99
- "(\\#[-a-z\\d_]*)?$", "i"); // fragment locator
100
- return !!pattern.test(input);
107
+ return urlPattern.test(input);
101
108
  };
109
+ const htmlPattern = /<([A-Za-z][A-Za-z0-9-]*)(\s+[A-Za-z-]+(\s*=\s*(?:".*?"|'.*?'|[^'">\s]+))?)*\s*>/;
102
110
  /**
103
111
  * Checks if the input contains HTML tags or entities.
104
112
  *
@@ -108,7 +116,7 @@ export const isURL = (input) => {
108
116
  * Note: Uses a regular expression to detect HTML tags (like <tag>) and entities (like &entity;).
109
117
  * Recognizes both opening and closing tags, as well as self-closing tags.
110
118
  */
111
- export const isHTML = (input) => /<\/?\s*[a-z-][^>]*\s*>|(\&(?:[\w\d]+|#\d+|#x[a-f\d]+);)/g.test(input);
119
+ export const isHTML = (input) => typeof input === "string" && htmlPattern.test(input);
112
120
  /**
113
121
  * Type guard function to check if the input is a string, excluding strings that match specific formats (number, boolean, email, URL, IP).
114
122
  *
@@ -119,6 +127,7 @@ export const isHTML = (input) => /<\/?\s*[a-z-][^>]*\s*>|(\&(?:[\w\d]+|#\d+|#x[a
119
127
  */
120
128
  export const isString = (input) => Object.prototype.toString.call(input) === "[object String]" &&
121
129
  (!isNumber(input) || String(input).at(0) === "0");
130
+ const ipPattern = /^(?:(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)\.){3}(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)$/;
122
131
  /**
123
132
  * Checks if the input is a valid IP address format.
124
133
  *
@@ -127,7 +136,7 @@ export const isString = (input) => Object.prototype.toString.call(input) === "[o
127
136
  *
128
137
  * Note: Uses a regular expression to validate IP addresses, ensuring they consist of four octets, each ranging from 0 to 255.
129
138
  */
130
- export const isIP = (input) => /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/.test(input);
139
+ export const isIP = (input) => typeof input === "string" && ipPattern.test(input);
131
140
  /**
132
141
  * Type guard function to check if the input is a boolean or a string representation of a boolean.
133
142
  *
@@ -152,7 +161,7 @@ export const isPassword = (input) => typeof input === "string" && input.length =
152
161
  * @param input - The input to be checked, can be of any type.
153
162
  * @returns A boolean indicating whether the input is a valid date.
154
163
  */
155
- export function isDate(input) {
164
+ export const isDate = (input) => {
156
165
  // Check if the input is null, undefined, or an empty string
157
166
  if (input == null || input === "")
158
167
  return false;
@@ -165,7 +174,7 @@ export function isDate(input) {
165
174
  const date = new Date(numTimestamp);
166
175
  // Check if the date is valid
167
176
  return date.getTime() === numTimestamp;
168
- }
177
+ };
169
178
  /**
170
179
  * Checks if the input is a valid ID.
171
180
  *
@@ -178,10 +187,34 @@ export const isValidID = (input) => {
178
187
  /**
179
188
  * Checks if a given string is a valid JSON.
180
189
  *
181
- * @param {string} str - The string to be checked.
190
+ * @param {string} input - The string to be checked.
182
191
  * @returns {boolean} Returns true if the string is valid JSON, otherwise false.
183
192
  */
184
- export const isStringified = (str) => str === "null" || str === "undefined" || str[0] === "{" || str[0] === "[";
193
+ export const isStringified = (input) => typeof input === "string" &&
194
+ (input === "null" ||
195
+ input === "undefined" ||
196
+ input[0] === "{" ||
197
+ input[0] === "[");
198
+ /**
199
+ * Recursively merges properties from a source object into a target object. If a property exists in both, the source's value overwrites the target's.
200
+ *
201
+ * @param target - The target object to merge properties into.
202
+ * @param source - The source object from which properties are merged.
203
+ * @returns any - The modified target object with merged properties.
204
+ *
205
+ * Note: Performs a deep merge for nested objects. Non-object properties are directly overwritten.
206
+ */
207
+ export const deepMerge = (target, source) => {
208
+ for (const key in source) {
209
+ if (Object.hasOwn(source, key)) {
210
+ if (isObject(source[key]) && isObject(target[key]))
211
+ target[key] = deepMerge(target[key], source[key]);
212
+ else if (source[key] !== null)
213
+ target[key] = source[key];
214
+ }
215
+ }
216
+ return target;
217
+ };
185
218
  /**
186
219
  * Identifies and returns properties that have changed between two objects.
187
220
  *
@@ -278,7 +311,7 @@ export const isFieldType = (compareAtType, fieldType, fieldChildrenType) => {
278
311
  // Function to recursively flatten an array of objects and their nested children
279
312
  export const flattenSchema = (schema, keepParents = false) => {
280
313
  const result = [];
281
- function _flattenHelper(item, parentKey) {
314
+ const _flattenHelper = (item, parentKey) => {
282
315
  if (item.children && isArrayOfObjects(item.children)) {
283
316
  if (keepParents)
284
317
  result.push((({ children, ...rest }) => rest)(item));
@@ -290,7 +323,7 @@ export const flattenSchema = (schema, keepParents = false) => {
290
323
  ...item,
291
324
  key: parentKey ? `${parentKey}.${item.key}` : item.key,
292
325
  });
293
- }
326
+ };
294
327
  for (const item of schema)
295
328
  _flattenHelper(item, "");
296
329
  return result;
@@ -372,7 +405,7 @@ export const validateFieldType = (value, fieldType, fieldChildrenType) => {
372
405
  return false;
373
406
  }
374
407
  };
375
- export function FormatObjectCriteriaValue(value) {
408
+ export const FormatObjectCriteriaValue = (value) => {
376
409
  switch (value[0]) {
377
410
  case ">":
378
411
  case "<":
@@ -420,7 +453,7 @@ export function FormatObjectCriteriaValue(value) {
420
453
  default:
421
454
  return ["=", value];
422
455
  }
423
- }
456
+ };
424
457
  /**
425
458
  * Get field from schema
426
459
  *
@@ -428,7 +461,7 @@ export function FormatObjectCriteriaValue(value) {
428
461
  * @param {string} keyPath Support dot notation path
429
462
  * @param {Schema} schema
430
463
  */
431
- export function getField(keyPath, schema) {
464
+ export const getField = (keyPath, schema) => {
432
465
  let RETURN = schema;
433
466
  const keyPathSplited = keyPath.split(".");
434
467
  for (const [index, key] of keyPathSplited.entries()) {
@@ -447,7 +480,7 @@ export function getField(keyPath, schema) {
447
480
  if (!RETURN)
448
481
  return null;
449
482
  return isArrayOfObjects(RETURN) ? RETURN[0] : RETURN;
450
- }
483
+ };
451
484
  /**
452
485
  * Override a schema field, key, type or other properties
453
486
  *
@@ -459,7 +492,7 @@ export function getField(keyPath, schema) {
459
492
  * type?: FieldType | FieldType[];
460
493
  * })} field
461
494
  */
462
- export function setField(keyPath, schema, field) {
495
+ export const setField = (keyPath, schema, field) => {
463
496
  const keyPathSplited = keyPath.split(".");
464
497
  for (const [index, key] of keyPathSplited.entries()) {
465
498
  const foundItem = schema.find((item) => item.key === key);
@@ -474,7 +507,7 @@ export function setField(keyPath, schema, field) {
474
507
  isArrayOfObjects(foundItem.children))
475
508
  schema = foundItem.children;
476
509
  }
477
- }
510
+ };
478
511
  /**
479
512
  * Remove field from schema
480
513
  *
@@ -482,7 +515,7 @@ export function setField(keyPath, schema, field) {
482
515
  * @param {string} keyPath Support dot notation path
483
516
  * @param {Schema} schema
484
517
  */
485
- export function unsetField(keyPath, schema) {
518
+ export const unsetField = (keyPath, schema) => {
486
519
  const keyPathSplited = keyPath.split(".");
487
520
  let parent = null;
488
521
  let targetIndex;
@@ -518,4 +551,4 @@ export function unsetField(keyPath, schema) {
518
551
  targetIndex = undefined;
519
552
  }
520
553
  }
521
- }
554
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inibase",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Karim Amahtil",
@@ -84,6 +84,7 @@
84
84
  "prepublish": "npx -q tsc",
85
85
  "build": "npx -q tsc",
86
86
  "benchmark": "./benchmark/run.js",
87
- "test": "npx -q tsx ./tests/inibase.test.ts"
87
+ "test": "npx -q tsx ./tests/inibase.test.ts",
88
+ "test:utils": "npx -q tsx ./tests/utils.test.ts"
88
89
  }
89
90
  }