inibase 1.1.10 → 1.1.12

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
@@ -97,6 +97,7 @@ interface {
97
97
  compression: boolean;
98
98
  cache: boolean;
99
99
  prepend: boolean;
100
+ decodeID: boolean;
100
101
  }
101
102
  ```
102
103
 
@@ -236,7 +237,8 @@ const db = new Inibase("/databaseName");
236
237
  const userTableConfig = {
237
238
  compression: true,
238
239
  cache: true,
239
- prepend: false
240
+ prepend: false,
241
+ decodeID: false
240
242
  }
241
243
 
242
244
  const userTableSchema = [
package/dist/cli.js CHANGED
@@ -102,7 +102,7 @@ rl.on("line", async (input) => {
102
102
  const configName = splitedInput[index].toLocaleLowerCase();
103
103
  if (["true", "false"].includes(configName))
104
104
  continue;
105
- if (!["compression", "cache", "prepend"].includes(configName)) {
105
+ if (!["compression", "cache", "prepend", "decodeID"].includes(configName)) {
106
106
  console.log(`${textRed(" Err:")} '${configName}' is not a valid config`);
107
107
  break;
108
108
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import "dotenv/config";
2
2
  export interface Data {
3
- id?: string;
3
+ id?: string | number;
4
4
  [key: string]: any;
5
5
  createdAt?: number;
6
6
  updatedAt?: number;
@@ -27,6 +27,7 @@ export interface Config {
27
27
  compression?: boolean;
28
28
  cache?: boolean;
29
29
  prepend?: boolean;
30
+ decodeID?: boolean;
30
31
  }
31
32
  export interface TableObject {
32
33
  schema?: Schema;
@@ -134,11 +135,11 @@ export default class Inibase {
134
135
  * @param {boolean} [onlyLinesNumbers]
135
136
  * @return {*} {(Promise<Data | number | (Data | number)[] | null>)}
136
137
  */
137
- get<TData extends Record<string, any> & Partial<Data>>(tableName: string, where: string | number | (string | number)[] | Criteria | undefined, options: Options | undefined, onlyOne: true, onlyLinesNumbers?: false): Promise<(Data & TData) | null>;
138
- get<TData extends Record<string, any> & Partial<Data>>(tableName: string, where: string | number, options?: Options, onlyOne?: boolean, onlyLinesNumbers?: false): Promise<(Data & TData) | null>;
139
- get<TData extends Record<string, any> & Partial<Data>>(tableName: string, where?: string | number | (string | number)[] | Criteria, options?: Options, onlyOne?: boolean, onlyLinesNumbers?: false): Promise<(Data & TData)[] | null>;
140
- get<TData extends Record<string, any> & Partial<Data>>(tableName: string, where: string | number | (string | number)[] | Criteria | undefined, options: Options | undefined, onlyOne: false | undefined, onlyLinesNumbers: true): Promise<number[] | null>;
141
- get<TData extends Record<string, any> & Partial<Data>>(tableName: string, where: string | number | (string | number)[] | Criteria | undefined, options: Options | undefined, onlyOne: true, onlyLinesNumbers: true): Promise<number | null>;
138
+ get<TData extends Record<string, any> & Partial<Data>>(tableName: string, where: string | number | (string | number)[] | Criteria | undefined, options: Options | undefined, onlyOne: true, onlyLinesNumbers?: false, _whereIsLinesNumbers?: boolean): Promise<(Data & TData) | null>;
139
+ get<TData extends Record<string, any> & Partial<Data>>(tableName: string, where: string | number, options?: Options, onlyOne?: boolean, onlyLinesNumbers?: false, _whereIsLinesNumbers?: boolean): Promise<(Data & TData) | null>;
140
+ get<TData extends Record<string, any> & Partial<Data>>(tableName: string, where?: string | number | (string | number)[] | Criteria, options?: Options, onlyOne?: boolean, onlyLinesNumbers?: false, _whereIsLinesNumbers?: boolean): Promise<(Data & TData)[] | null>;
141
+ get<TData extends Record<string, any> & Partial<Data>>(tableName: string, where: string | number | (string | number)[] | Criteria | undefined, options: Options | undefined, onlyOne: false | undefined, onlyLinesNumbers: true, _whereIsLinesNumbers?: boolean): Promise<number[] | null>;
142
+ get<TData extends Record<string, any> & Partial<Data>>(tableName: string, where: string | number | (string | number)[] | Criteria | undefined, options: Options | undefined, onlyOne: true, onlyLinesNumbers: true, _whereIsLinesNumbers?: boolean): Promise<number | null>;
142
143
  /**
143
144
  * Create new item(s) in a table
144
145
  *
@@ -161,10 +162,10 @@ export default class Inibase {
161
162
  * @param {boolean} [returnUpdatedData]
162
163
  * @return {*} {Promise<Data | Data[] | null | undefined | void>}
163
164
  */
164
- put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data?: (Data & TData) | (Data & TData)[], where?: number | string | (number | string)[] | Criteria | undefined, options?: Options | undefined, returnUpdatedData?: false): Promise<void>;
165
- put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: Data & TData, where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean): Promise<(Data & TData) | null>;
166
- put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData)[], where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean): Promise<(Data & TData)[] | null>;
167
- put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData) | (Data & TData)[], where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean): Promise<(Data & TData) | (Data & TData)[] | null>;
165
+ put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data?: (Data & TData) | (Data & TData)[], where?: number | string | (number | string)[] | Criteria | undefined, options?: Options | undefined, returnUpdatedData?: false, _whereIsLinesNumbers?: boolean): Promise<void>;
166
+ put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: Data & TData, where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean, _whereIsLinesNumbers?: boolean): Promise<(Data & TData) | null>;
167
+ put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData)[], where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean, _whereIsLinesNumbers?: boolean): Promise<(Data & TData)[] | null>;
168
+ put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData) | (Data & TData)[], where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean, _whereIsLinesNumbers?: boolean): Promise<(Data & TData) | (Data & TData)[] | null>;
168
169
  /**
169
170
  * Delete item(s) in a table
170
171
  *
@@ -172,7 +173,7 @@ export default class Inibase {
172
173
  * @param {(number | string | (number | string)[] | Criteria)} [where]
173
174
  * @return {boolean | null} {(Promise<boolean | null>)}
174
175
  */
175
- delete(tableName: string, where?: number | string | (number | string)[] | Criteria, _id?: string | string[]): Promise<boolean | null>;
176
+ delete(tableName: string, where?: number | string | (number | string)[] | Criteria, _whereIsLinesNumbers?: boolean): Promise<boolean | null>;
176
177
  /**
177
178
  * Generate sum of column(s) in a table
178
179
  *
package/dist/index.js CHANGED
@@ -158,6 +158,7 @@ export default class Inibase {
158
158
  compression: process.env.INIBASE_COMPRESSION == "true",
159
159
  cache: process.env.INIBASE_CACHE === "true",
160
160
  prepend: process.env.INIBASE_PREPEND === "true",
161
+ decodeID: process.env.INIBASE_ENCODEID === "true",
161
162
  };
162
163
  if (config) {
163
164
  if (config.compression)
@@ -251,6 +252,13 @@ export default class Inibase {
251
252
  await unlink(join(tablePath, ".cache.config"));
252
253
  }
253
254
  }
255
+ if (config.decodeID !== undefined &&
256
+ config.decodeID !== table.config.decodeID) {
257
+ if (config.decodeID)
258
+ await writeFile(join(tablePath, ".decodeID.config"), "");
259
+ else
260
+ await unlink(join(tablePath, ".decodeID.config"));
261
+ }
254
262
  if (config.prepend !== undefined &&
255
263
  config.prepend !== table.config.prepend) {
256
264
  await UtilsServer.execFile("find", [
@@ -302,6 +310,7 @@ export default class Inibase {
302
310
  compression: await File.isExists(join(tablePath, ".compression.config")),
303
311
  cache: await File.isExists(join(tablePath, ".cache.config")),
304
312
  prepend: await File.isExists(join(tablePath, ".prepend.config")),
313
+ decodeID: await File.isExists(join(tablePath, ".decodeID.config")),
305
314
  },
306
315
  });
307
316
  return this.tablesMap.get(tableName);
@@ -717,7 +726,9 @@ export default class Inibase {
717
726
  async processSimpleField(tableName, field, linesNumber, RETURN, _options, prefix) {
718
727
  const fieldPath = join(this.databasePath, tableName, `${prefix ?? ""}${field.key}${this.getFileExtension(tableName)}`);
719
728
  if (await File.isExists(fieldPath)) {
720
- const items = await File.get(fieldPath, linesNumber, field.type, field.children, this.salt);
729
+ const items = await File.get(fieldPath, linesNumber, field.key === "id" && this.tablesMap.get(tableName).config.decodeID
730
+ ? "number"
731
+ : field.type, field.children, this.salt);
721
732
  if (items) {
722
733
  for (const [index, item] of Object.entries(items)) {
723
734
  if (typeof item === "undefined")
@@ -991,7 +1002,9 @@ export default class Inibase {
991
1002
  searchOperator = "=";
992
1003
  searchComparedAtValue = value;
993
1004
  }
994
- const [searchResult, totalLines, linesNumbers] = await File.search(join(tablePath, `${key}${this.getFileExtension(tableName)}`), searchOperator ?? "=", searchComparedAtValue ?? null, searchLogicalOperator, allTrue ? searchIn : undefined, field?.type, field?.children, options.perPage, (options.page - 1) * options.perPage + 1, true, this.salt);
1005
+ const [searchResult, totalLines, linesNumbers] = await File.search(join(tablePath, `${key}${this.getFileExtension(tableName)}`), searchOperator ?? "=", searchComparedAtValue ?? null, searchLogicalOperator, allTrue ? searchIn : undefined, field?.type, field?.children, options.perPage, (options.page - 1) * options.perPage + 1, true, field.key === "id" && this.tablesMap.get(tableName).config.decodeID
1006
+ ? undefined
1007
+ : this.salt);
995
1008
  if (searchResult) {
996
1009
  const formatedSearchResult = Object.fromEntries(Object.entries(searchResult).map(([id, value]) => {
997
1010
  const nestedObj = {};
@@ -1062,7 +1075,7 @@ export default class Inibase {
1062
1075
  async get(tableName, where, options = {
1063
1076
  page: 1,
1064
1077
  perPage: 15,
1065
- }, onlyOne, onlyLinesNumbers) {
1078
+ }, onlyOne, onlyLinesNumbers, _whereIsLinesNumbers) {
1066
1079
  const tablePath = join(this.databasePath, tableName);
1067
1080
  // Ensure options.columns is an array
1068
1081
  if (options.columns) {
@@ -1122,14 +1135,16 @@ export default class Inibase {
1122
1135
  1)
1123
1136
  .map((lineNumber) => `NR==${lineNumber}`)
1124
1137
  .join(" || ")}'`;
1125
- const filesPathes = [["id", true], ...sortArray].map((column) => join(tablePath, `${column[0]}${this.getFileExtension(tableName)}`));
1138
+ const filesPathes = (sortArray.find(([key]) => key === "id")
1139
+ ? sortArray
1140
+ : [["id", true], ...sortArray]).map((column) => join(tablePath, `${column[0]}${this.getFileExtension(tableName)}`));
1126
1141
  for await (const path of filesPathes.slice(1))
1127
1142
  if (!(await File.isExists(path)))
1128
1143
  return null;
1129
1144
  // Construct the paste command to merge files and filter lines by IDs
1130
1145
  const pasteCommand = `paste '${filesPathes.join("' '")}'`;
1131
1146
  // Construct the sort command dynamically based on the number of files for sorting
1132
- const index = 2;
1147
+ const index = 1;
1133
1148
  const sortColumns = sortArray
1134
1149
  .map(([key, ascending], i) => {
1135
1150
  const field = Utils.getField(key, schema);
@@ -1167,8 +1182,13 @@ export default class Inibase {
1167
1182
  // Extract values for each file, including `id${this.getFileExtension(tableName)}`
1168
1183
  filesPathes.forEach((fileName, index) => {
1169
1184
  const field = Utils.getField(parse(fileName).name, schema);
1170
- if (field)
1171
- outputObject[field.key] = File.decode(splitedFileColumns[index], field?.type, field?.children, this.salt);
1185
+ if (field) {
1186
+ if (field.key === "id" &&
1187
+ this.tablesMap.get(tableName).config.decodeID)
1188
+ outputObject[field.key] = splitedFileColumns[index];
1189
+ else
1190
+ outputObject[field.key] = File.decode(splitedFileColumns[index], field?.type, field?.children, this.salt);
1191
+ }
1172
1192
  });
1173
1193
  return outputObject;
1174
1194
  });
@@ -1176,7 +1196,7 @@ export default class Inibase {
1176
1196
  return restOfColumns
1177
1197
  ? outputArray.map((item) => ({
1178
1198
  ...item,
1179
- ...restOfColumns.find(({ id }) => id === item.id),
1199
+ ...restOfColumns.find(({ id }) => id === (Utils.isNumber(item.id) ? Number(item.id) : item.id)),
1180
1200
  }))
1181
1201
  : outputArray;
1182
1202
  }
@@ -1193,8 +1213,9 @@ export default class Inibase {
1193
1213
  if (!this.totalItems.has(`${tableName}-*`))
1194
1214
  this.totalItems.set(`${tableName}-*`, pagination[1]);
1195
1215
  }
1196
- else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1197
- Utils.isNumber(where)) {
1216
+ else if (((Array.isArray(where) && where.every(Utils.isNumber)) ||
1217
+ Utils.isNumber(where)) &&
1218
+ (_whereIsLinesNumbers || !this.tablesMap.get(tableName).config.decodeID)) {
1198
1219
  // "where" in this case, is the line(s) number(s) and not id(s)
1199
1220
  let lineNumbers = where;
1200
1221
  if (!Array.isArray(lineNumbers))
@@ -1208,7 +1229,11 @@ export default class Inibase {
1208
1229
  if (RETURN?.length && !Array.isArray(where))
1209
1230
  RETURN = RETURN[0];
1210
1231
  }
1211
- else if ((Array.isArray(where) && where.every(Utils.isValidID)) ||
1232
+ else if ((!_whereIsLinesNumbers &&
1233
+ this.tablesMap.get(tableName).config.decodeID &&
1234
+ ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1235
+ Utils.isNumber(where))) ||
1236
+ (Array.isArray(where) && where.every(Utils.isValidID)) ||
1212
1237
  Utils.isValidID(where)) {
1213
1238
  let Ids = where;
1214
1239
  if (!Array.isArray(Ids))
@@ -1245,7 +1270,7 @@ export default class Inibase {
1245
1270
  return onlyOne ? Number(cachedItems[0]) : cachedItems.map(Number);
1246
1271
  return this.get(tableName, cachedItems
1247
1272
  .slice((options.page - 1) * options.perPage, options.page * options.perPage)
1248
- .map(Number), options, onlyOne);
1273
+ .map(Number), options, onlyOne, undefined, true);
1249
1274
  }
1250
1275
  const [LineNumberDataMap, linesNumbers] = await this.applyCriteria(tableName, options, where);
1251
1276
  if (LineNumberDataMap && linesNumbers?.size) {
@@ -1279,6 +1304,15 @@ export default class Inibase {
1279
1304
  totalPages: Math.ceil(greatestTotalItems / options.perPage),
1280
1305
  total: greatestTotalItems,
1281
1306
  };
1307
+ // if (this.tablesMap.get(tableName).config.decodeID) {
1308
+ // if (Array.isArray(RETURN)) {
1309
+ // for (let index = 0; index < RETURN.length; index++)
1310
+ // RETURN[index].id = UtilsServer.decodeID(
1311
+ // RETURN[index].id as string,
1312
+ // this.salt,
1313
+ // );
1314
+ // } else RETURN.id = UtilsServer.decodeID(RETURN.id as string, this.salt);
1315
+ // }
1282
1316
  return onlyOne && Array.isArray(RETURN) ? RETURN[0] : RETURN;
1283
1317
  }
1284
1318
  async post(tableName, data, options, returnPostedData) {
@@ -1344,7 +1378,8 @@ export default class Inibase {
1344
1378
  ? clonedData
1345
1379
  .map((_, index) => this.totalItems.get(`${tableName}-*`) - index)
1346
1380
  .toReversed()
1347
- : this.totalItems.get(`${tableName}-*`), options, !Utils.isArrayOfObjects(clonedData));
1381
+ : this.totalItems.get(`${tableName}-*`), options, !Utils.isArrayOfObjects(clonedData), // return only one item if data is not array of objects
1382
+ undefined, true);
1348
1383
  }
1349
1384
  finally {
1350
1385
  if (renameList.length)
@@ -1355,7 +1390,7 @@ export default class Inibase {
1355
1390
  async put(tableName, data, where, options = {
1356
1391
  page: 1,
1357
1392
  perPage: 15,
1358
- }, returnUpdatedData) {
1393
+ }, returnUpdatedData, _whereIsLinesNumbers) {
1359
1394
  const renameList = [];
1360
1395
  const tablePath = join(this.databasePath, tableName);
1361
1396
  await this.throwErrorIfTableEmpty(tableName);
@@ -1398,13 +1433,9 @@ export default class Inibase {
1398
1433
  await File.unlock(join(tablePath, ".tmp"));
1399
1434
  }
1400
1435
  }
1401
- else if ((Array.isArray(where) && where.every(Utils.isValidID)) ||
1402
- Utils.isValidID(where)) {
1403
- const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1404
- return this.put(tableName, clonedData, lineNumbers, options, false);
1405
- }
1406
- else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1407
- Utils.isNumber(where)) {
1436
+ else if (((Array.isArray(where) && where.every(Utils.isNumber)) ||
1437
+ Utils.isNumber(where)) &&
1438
+ (_whereIsLinesNumbers || !this.tablesMap.get(tableName).config.decodeID)) {
1408
1439
  // "where" in this case, is the line(s) number(s) and not id(s)
1409
1440
  await this.validateData(tableName, clonedData, true);
1410
1441
  clonedData = this.formatData(clonedData, this.tablesMap.get(tableName).schema, true);
@@ -1432,7 +1463,7 @@ export default class Inibase {
1432
1463
  if (this.tablesMap.get(tableName).config.cache)
1433
1464
  await this.clearCache(tableName);
1434
1465
  if (returnUpdatedData)
1435
- return this.get(tableName, where, options, !Array.isArray(where));
1466
+ return this.get(tableName, where, options, !Array.isArray(where), undefined, true);
1436
1467
  }
1437
1468
  finally {
1438
1469
  if (renameList.length)
@@ -1440,10 +1471,19 @@ export default class Inibase {
1440
1471
  await File.unlock(join(tablePath, ".tmp"), keys);
1441
1472
  }
1442
1473
  }
1474
+ else if ((!_whereIsLinesNumbers &&
1475
+ this.tablesMap.get(tableName).config.decodeID &&
1476
+ ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1477
+ Utils.isNumber(where))) ||
1478
+ (Array.isArray(where) && where.every(Utils.isValidID)) ||
1479
+ Utils.isValidID(where)) {
1480
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1481
+ return this.put(tableName, clonedData, lineNumbers, options, false, true);
1482
+ }
1443
1483
  else if (Utils.isObject(where)) {
1444
1484
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1445
1485
  if (lineNumbers)
1446
- return this.put(tableName, clonedData, lineNumbers, options, returnUpdatedData);
1486
+ return this.put(tableName, clonedData, lineNumbers, options, returnUpdatedData, true);
1447
1487
  }
1448
1488
  else
1449
1489
  throw this.createError("INVALID_PARAMETERS");
@@ -1455,7 +1495,7 @@ export default class Inibase {
1455
1495
  * @param {(number | string | (number | string)[] | Criteria)} [where]
1456
1496
  * @return {boolean | null} {(Promise<boolean | null>)}
1457
1497
  */
1458
- async delete(tableName, where, _id) {
1498
+ async delete(tableName, where, _whereIsLinesNumbers) {
1459
1499
  const tablePath = join(this.databasePath, tableName);
1460
1500
  await this.throwErrorIfTableEmpty(tableName);
1461
1501
  if (!where) {
@@ -1483,13 +1523,9 @@ export default class Inibase {
1483
1523
  await File.unlock(join(tablePath, ".tmp"));
1484
1524
  }
1485
1525
  }
1486
- if ((Array.isArray(where) && where.every(Utils.isValidID)) ||
1487
- Utils.isValidID(where)) {
1488
- const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1489
- return this.delete(tableName, lineNumbers, where);
1490
- }
1491
- if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1492
- Utils.isNumber(where)) {
1526
+ if (((Array.isArray(where) && where.every(Utils.isNumber)) ||
1527
+ Utils.isNumber(where)) &&
1528
+ (_whereIsLinesNumbers || !this.tablesMap.get(tableName).config.decodeID)) {
1493
1529
  // "where" in this case, is the line(s) number(s) and not id(s)
1494
1530
  const files = (await readdir(tablePath))?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)));
1495
1531
  if (files.length) {
@@ -1529,10 +1565,19 @@ export default class Inibase {
1529
1565
  }
1530
1566
  }
1531
1567
  }
1532
- else if (Utils.isObject(where)) {
1568
+ if ((!_whereIsLinesNumbers &&
1569
+ this.tablesMap.get(tableName).config.decodeID &&
1570
+ ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1571
+ Utils.isNumber(where))) ||
1572
+ (Array.isArray(where) && where.every(Utils.isValidID)) ||
1573
+ Utils.isValidID(where)) {
1574
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1575
+ return this.delete(tableName, lineNumbers, true);
1576
+ }
1577
+ if (Utils.isObject(where)) {
1533
1578
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1534
1579
  if (lineNumbers)
1535
- return this.delete(tableName, lineNumbers);
1580
+ return this.delete(tableName, lineNumbers, true);
1536
1581
  }
1537
1582
  else
1538
1583
  throw this.createError("INVALID_PARAMETERS");
@@ -221,6 +221,9 @@ export const isEqual = (originalValue, comparedValue, fieldType) => {
221
221
  // If both are null-like, treat as equivalent
222
222
  if (isOriginalNullLike && isComparedNullLike)
223
223
  return true;
224
+ // If both are number-like
225
+ if (isNumber(originalValue) && isNumber(comparedValue))
226
+ return Number(originalValue) === Number(comparedValue);
224
227
  // Direct equality check for other cases
225
228
  return originalValue === comparedValue;
226
229
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inibase",
3
- "version": "1.1.10",
3
+ "version": "1.1.12",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Karim Amahtil",