inibase 1.6.6 → 1.7.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/file.js +217 -2
- package/dist/index.js +1 -1
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +27 -14
- package/package.json +1 -1
package/dist/file.js
CHANGED
|
@@ -6,8 +6,8 @@ import { pipeline } from "node:stream/promises";
|
|
|
6
6
|
import { createGunzip, createGzip } from "node:zlib";
|
|
7
7
|
import Inison from "inison";
|
|
8
8
|
import { globalConfig, } from "./index.js";
|
|
9
|
-
import { detectFieldType, isArrayOfObjects, isNumber, isObject, isStringified, } from "./utils.js";
|
|
10
|
-
import { compare, encodeID, exec, gunzip, gzip } from "./utils.server.js";
|
|
9
|
+
import { detectFieldType, isArrayOfObjects, isNumber, isObject, isStringified, isValidID, } from "./utils.js";
|
|
10
|
+
import { compare, decodeID, encodeID, exec, gunzip, gzip, } from "./utils.server.js";
|
|
11
11
|
// Locks older than this are assumed abandoned by a crashed/killed process, not a slow operation.
|
|
12
12
|
const STALE_LOCK_MS = 30_000;
|
|
13
13
|
export const lock = async (folderPath, prefix) => {
|
|
@@ -506,6 +506,205 @@ export const remove = async (filePath, linesToDelete) => {
|
|
|
506
506
|
return [fileTempPath, null];
|
|
507
507
|
}
|
|
508
508
|
};
|
|
509
|
+
/**
|
|
510
|
+
* Field types whose on-disk bytes equal the encoded query value (identity or
|
|
511
|
+
* canonical numeric/id transforms), making a native whole-line equality search safe.
|
|
512
|
+
*/
|
|
513
|
+
const EQUALS_FAST_TYPES = new Set([
|
|
514
|
+
"string",
|
|
515
|
+
"text",
|
|
516
|
+
"textarea",
|
|
517
|
+
"html",
|
|
518
|
+
"url",
|
|
519
|
+
"email",
|
|
520
|
+
"number",
|
|
521
|
+
"date",
|
|
522
|
+
"timestamp",
|
|
523
|
+
"time",
|
|
524
|
+
"id",
|
|
525
|
+
"table",
|
|
526
|
+
]);
|
|
527
|
+
/**
|
|
528
|
+
* True when the query value is, or looks like, a number. Such values alias across
|
|
529
|
+
* several raw byte forms under `decode` (e.g. "123", "0123", "1e3"), so the exact
|
|
530
|
+
* native match would silently drop legitimate lines -> fall back to the JS reader.
|
|
531
|
+
*/
|
|
532
|
+
const looksNumeric = (value) => typeof value === "number" ||
|
|
533
|
+
(typeof value === "string" && value !== "" && isNumber(value));
|
|
534
|
+
const shellQuote = (str) => `'${String(str).replace(/'/g, `'\\''`)}'`;
|
|
535
|
+
/**
|
|
536
|
+
* Computes the exact on-disk byte strings a native `grep -x -F` must match for a
|
|
537
|
+
* `=` (equality) search so that every matched line decodes to one of the compared
|
|
538
|
+
* values. Returns the pattern array, or null when the search cannot be handled by
|
|
539
|
+
* the fast path (caller then falls back to the JS readline scan).
|
|
540
|
+
*/
|
|
541
|
+
function buildEqualsPatterns(comparedAtValue, field) {
|
|
542
|
+
const values = Array.isArray(comparedAtValue)
|
|
543
|
+
? comparedAtValue
|
|
544
|
+
: [comparedAtValue];
|
|
545
|
+
if (!values.length)
|
|
546
|
+
return null;
|
|
547
|
+
const patterns = [];
|
|
548
|
+
for (const value of values) {
|
|
549
|
+
if (value === null || value === undefined || value === "")
|
|
550
|
+
return null; // null-like values -> readline path
|
|
551
|
+
const type = field.type;
|
|
552
|
+
if (type === "id" || type === "table") {
|
|
553
|
+
let forms;
|
|
554
|
+
if (isValidID(value))
|
|
555
|
+
forms = [value, String(decodeID(value) ?? value)];
|
|
556
|
+
else if (typeof value === "number" ||
|
|
557
|
+
(typeof value === "string" && isNumber(value)))
|
|
558
|
+
forms = [String(Number(value))];
|
|
559
|
+
else
|
|
560
|
+
forms = [value];
|
|
561
|
+
for (const form of [...new Set(forms)]) {
|
|
562
|
+
let valid = false;
|
|
563
|
+
try {
|
|
564
|
+
valid = compare("=", decode(form, field), value, type);
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
valid = false;
|
|
568
|
+
}
|
|
569
|
+
if (!valid)
|
|
570
|
+
return null;
|
|
571
|
+
patterns.push(form);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
else if (type === "number" ||
|
|
575
|
+
type === "date" ||
|
|
576
|
+
type === "timestamp" ||
|
|
577
|
+
type === "time") {
|
|
578
|
+
const form = String(Number(value));
|
|
579
|
+
let valid = false;
|
|
580
|
+
try {
|
|
581
|
+
valid = compare("=", decode(form, field), value, type);
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
valid = false;
|
|
585
|
+
}
|
|
586
|
+
if (!valid)
|
|
587
|
+
return null;
|
|
588
|
+
patterns.push(form);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
// scalar string-like field types
|
|
592
|
+
if (looksNumeric(value))
|
|
593
|
+
return null; // numeric aliasing -> readline path
|
|
594
|
+
const raw = String(value);
|
|
595
|
+
if (raw === "null" ||
|
|
596
|
+
raw === "undefined" ||
|
|
597
|
+
raw.startsWith("{") ||
|
|
598
|
+
raw.startsWith("["))
|
|
599
|
+
return null;
|
|
600
|
+
let encoded;
|
|
601
|
+
try {
|
|
602
|
+
encoded = encode(value);
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const form = String(encoded);
|
|
608
|
+
let valid = false;
|
|
609
|
+
try {
|
|
610
|
+
valid = compare("=", decode(form, field), value, type);
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
valid = false;
|
|
614
|
+
}
|
|
615
|
+
if (!valid)
|
|
616
|
+
return null;
|
|
617
|
+
patterns.push(form);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return [...new Set(patterns)];
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Native whole-line equality search (`grep -a -n -x -F`). Returns the same tuple
|
|
624
|
+
* shape as `search`, or null when the fast path could not be used/verified safely
|
|
625
|
+
* (in which case the caller should fall back to the JS readline scan).
|
|
626
|
+
*/
|
|
627
|
+
async function searchEqualsNative(filePath, patterns, field, searchIn, comparedAtValue, limit, offset, readWholeFile) {
|
|
628
|
+
const totalPatternSize = patterns.reduce((acc, pattern) => acc + String(pattern).length, 0) +
|
|
629
|
+
patterns.length * 8;
|
|
630
|
+
if (totalPatternSize > 32_768 || patterns.length > 1024)
|
|
631
|
+
return null;
|
|
632
|
+
if (searchIn?.size) {
|
|
633
|
+
for (const lineNumber of searchIn) {
|
|
634
|
+
if (lineNumber < 0)
|
|
635
|
+
return null; // exclusion ranges -> readline path
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const source = filePath.endsWith(".gz")
|
|
639
|
+
? `gunzip -c ${escapeShellPath(filePath)}`
|
|
640
|
+
: `cat ${escapeShellPath(filePath)}`;
|
|
641
|
+
// Column files whose lines start with "[" or "{" are array/object-encoded
|
|
642
|
+
// (decode() eagerly unstringifies them), so a native whole-line grep could
|
|
643
|
+
// silently miss matches that live inside those containers. Detect such lines
|
|
644
|
+
// up front; when any are present, fall back to the JS reader.
|
|
645
|
+
let probe;
|
|
646
|
+
try {
|
|
647
|
+
({ stdout: probe } = await exec(`LC_ALL=C ${source} | LC_ALL=C grep -a -m 1 -E '^[[{]'`, { maxBuffer: 1024 * 1024 * 4 }));
|
|
648
|
+
}
|
|
649
|
+
catch (err) {
|
|
650
|
+
if (Number(err?.code) !== 1)
|
|
651
|
+
return null; // probe failure -> readline fallback
|
|
652
|
+
}
|
|
653
|
+
if (probe)
|
|
654
|
+
return null;
|
|
655
|
+
const command = `LC_ALL=C ${source} | LC_ALL=C grep -a -n -x -F ${patterns
|
|
656
|
+
.map((pattern) => `-e ${shellQuote(pattern)}`)
|
|
657
|
+
.join(" ")}`;
|
|
658
|
+
let stdout;
|
|
659
|
+
try {
|
|
660
|
+
({ stdout } = await exec(command, { maxBuffer: 1024 * 1024 * 256 }));
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
if (Number(err?.code) === 1)
|
|
664
|
+
return [null, 0, null]; // grep: no matches
|
|
665
|
+
return null; // native failure -> readline fallback
|
|
666
|
+
}
|
|
667
|
+
const rawLines = stdout ? stdout.trimEnd().split("\n") : [];
|
|
668
|
+
let matched = [];
|
|
669
|
+
for (const line of rawLines) {
|
|
670
|
+
const colonIndex = line.indexOf(":");
|
|
671
|
+
if (colonIndex === -1)
|
|
672
|
+
continue;
|
|
673
|
+
const lineNumber = Number(line.slice(0, colonIndex));
|
|
674
|
+
if (!Number.isInteger(lineNumber) || lineNumber < 1)
|
|
675
|
+
continue;
|
|
676
|
+
matched.push([lineNumber, line.slice(colonIndex + 1)]);
|
|
677
|
+
}
|
|
678
|
+
if (searchIn?.size)
|
|
679
|
+
matched = matched.filter(([lineNumber]) => searchIn.has(lineNumber));
|
|
680
|
+
const linesNumbers = new Set();
|
|
681
|
+
const matchingLines = {};
|
|
682
|
+
let processed = 0;
|
|
683
|
+
let finalTotal = null;
|
|
684
|
+
for (const [lineNumber, raw] of matched) {
|
|
685
|
+
processed++;
|
|
686
|
+
linesNumbers.add(lineNumber);
|
|
687
|
+
if (offset && processed < offset)
|
|
688
|
+
continue;
|
|
689
|
+
if (limit && processed > limit + (offset ? offset - 1 : 0)) {
|
|
690
|
+
if (readWholeFile)
|
|
691
|
+
continue;
|
|
692
|
+
finalTotal = processed;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
const decodedLine = decode(raw, field);
|
|
696
|
+
// Defensive verification: the raw line must decode to one of the compared
|
|
697
|
+
// values (guaranteed by construction, kept as a safety net).
|
|
698
|
+
const verifies = Array.isArray(comparedAtValue)
|
|
699
|
+
? comparedAtValue.some((value) => compare("=", decodedLine, value, field.type))
|
|
700
|
+
: compare("=", decodedLine, comparedAtValue, field.type);
|
|
701
|
+
if (!verifies)
|
|
702
|
+
return null;
|
|
703
|
+
matchingLines[lineNumber] = decodedLine;
|
|
704
|
+
}
|
|
705
|
+
const total = finalTotal ?? processed;
|
|
706
|
+
return total ? [matchingLines, total, linesNumbers] : [null, 0, null];
|
|
707
|
+
}
|
|
509
708
|
/**
|
|
510
709
|
* Asynchronously searches a file for lines matching specified criteria, using comparison and logical operators.
|
|
511
710
|
*
|
|
@@ -524,6 +723,22 @@ export const remove = async (filePath, linesToDelete) => {
|
|
|
524
723
|
* Note: Decodes each line for comparison and can handle complex queries with multiple conditions.
|
|
525
724
|
*/
|
|
526
725
|
export const search = async (filePath, operator, comparedAtValue, logicalOperator, searchIn, field, limit, offset, readWholeFile) => {
|
|
726
|
+
// Native fast path for exact-equality searches (whole-line `grep -x -F`).
|
|
727
|
+
const fieldType = field?.type;
|
|
728
|
+
if (operator === "=" &&
|
|
729
|
+
!Array.isArray(operator) &&
|
|
730
|
+
!logicalOperator &&
|
|
731
|
+
comparedAtValue !== null &&
|
|
732
|
+
comparedAtValue !== undefined &&
|
|
733
|
+
typeof fieldType === "string" &&
|
|
734
|
+
EQUALS_FAST_TYPES.has(fieldType)) {
|
|
735
|
+
const patterns = buildEqualsPatterns(comparedAtValue, field);
|
|
736
|
+
if (patterns) {
|
|
737
|
+
const fastResult = await searchEqualsNative(filePath, patterns, field, searchIn, comparedAtValue, limit, offset, readWholeFile);
|
|
738
|
+
if (fastResult)
|
|
739
|
+
return fastResult;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
527
742
|
// Initialize a Map to store the matching lines with their line numbers.
|
|
528
743
|
const matchingLines = {};
|
|
529
744
|
// Initialize counters for line number, found items, and processed items.
|
package/dist/index.js
CHANGED
|
@@ -48,7 +48,7 @@ export default class Inibase {
|
|
|
48
48
|
this.uniqueMap = new Map();
|
|
49
49
|
if (!globalConfig[this.databasePath])
|
|
50
50
|
globalConfig[this.databasePath] = { tables: new Map() };
|
|
51
|
-
if (!process
|
|
51
|
+
if (!process?.env.INIBASE_SECRET) {
|
|
52
52
|
if (existsSync(".env") &&
|
|
53
53
|
readFileSync(".env").includes("INIBASE_SECRET="))
|
|
54
54
|
throw this.createError("NO_ENV");
|
package/dist/utils.d.ts
CHANGED
|
@@ -221,7 +221,9 @@ export declare function addIdToSchema(schema: Schema, startWithID: {
|
|
|
221
221
|
/**
|
|
222
222
|
* Translated error messages for every supported language and error code.
|
|
223
223
|
* The `{variable}` placeholder is replaced with the relevant value by
|
|
224
|
-
* {@link createError}.
|
|
224
|
+
* {@link createError}. `NO_ENV` is resolved lazily by {@link createError}
|
|
225
|
+
* via {@link NO_ENV_MESSAGES} since it depends on the Node.js version and
|
|
226
|
+
* must stay safe to evaluate outside of Node (e.g. in a browser bundle).
|
|
225
227
|
*/
|
|
226
228
|
export declare const ERROR_MESSAGES: Record<ErrorLang, Record<ErrorCode, string>>;
|
|
227
229
|
/**
|
package/dist/utils.js
CHANGED
|
@@ -628,10 +628,29 @@ export function addIdToSchema(schema, startWithID) {
|
|
|
628
628
|
const addIdToSchemaHelper = (schema) => schema.map(addIdToField);
|
|
629
629
|
return addIdToSchemaHelper(clonedSchema);
|
|
630
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* Whether the current Node.js runtime supports the `--env-file` CLI flag
|
|
633
|
+
* (added in Node 20.6, stabilized in later versions). Safe to call outside
|
|
634
|
+
* of Node (e.g. in a browser bundle), where it simply returns `false`.
|
|
635
|
+
*/
|
|
636
|
+
const supportsEnvFileFlag = () => {
|
|
637
|
+
if (typeof process === "undefined" || !process.versions?.node)
|
|
638
|
+
return false;
|
|
639
|
+
const [major] = process.versions.node.split(".").map(Number);
|
|
640
|
+
return major >= 20;
|
|
641
|
+
};
|
|
642
|
+
const NO_ENV_MESSAGES = {
|
|
643
|
+
en: ["please run with '--env-file=.env'", "please use dotenv"],
|
|
644
|
+
ar: ["يرجى التشغيل باستخدام '--env-file=.env'", "يرجى استخدام dotenv"],
|
|
645
|
+
fr: ["veuillez exécuter avec '--env-file=.env'", "veuillez utiliser dotenv"],
|
|
646
|
+
es: ["por favor ejecute con '--env-file=.env'", "por favor use dotenv"],
|
|
647
|
+
};
|
|
631
648
|
/**
|
|
632
649
|
* Translated error messages for every supported language and error code.
|
|
633
650
|
* The `{variable}` placeholder is replaced with the relevant value by
|
|
634
|
-
* {@link createError}.
|
|
651
|
+
* {@link createError}. `NO_ENV` is resolved lazily by {@link createError}
|
|
652
|
+
* via {@link NO_ENV_MESSAGES} since it depends on the Node.js version and
|
|
653
|
+
* must stay safe to evaluate outside of Node (e.g. in a browser bundle).
|
|
635
654
|
*/
|
|
636
655
|
export const ERROR_MESSAGES = {
|
|
637
656
|
en: {
|
|
@@ -647,9 +666,7 @@ export const ERROR_MESSAGES = {
|
|
|
647
666
|
INVALID_PARAMETERS: "The given parameters are not valid",
|
|
648
667
|
INVALID_REGEX_MATCH: "Field {variable} does not match the expected pattern",
|
|
649
668
|
INVALID_NAME: "Name {variable} is not valid",
|
|
650
|
-
NO_ENV:
|
|
651
|
-
? "please run with '--env-file=.env'"
|
|
652
|
-
: "please use dotenv",
|
|
669
|
+
NO_ENV: "",
|
|
653
670
|
},
|
|
654
671
|
ar: {
|
|
655
672
|
TABLE_EMPTY: "الجدول {variable} فارغ",
|
|
@@ -664,9 +681,7 @@ export const ERROR_MESSAGES = {
|
|
|
664
681
|
INVALID_PARAMETERS: "المعلمات المقدمة غير صالحة",
|
|
665
682
|
INVALID_REGEX_MATCH: "الحقل {variable} لا يتطابق مع النمط المتوقع",
|
|
666
683
|
INVALID_NAME: "الاسم {variable} غير صالح",
|
|
667
|
-
NO_ENV:
|
|
668
|
-
? "يرجى التشغيل باستخدام '--env-file=.env'"
|
|
669
|
-
: "يرجى استخدام dotenv",
|
|
684
|
+
NO_ENV: "",
|
|
670
685
|
},
|
|
671
686
|
fr: {
|
|
672
687
|
TABLE_EMPTY: "La table {variable} est vide",
|
|
@@ -681,9 +696,7 @@ export const ERROR_MESSAGES = {
|
|
|
681
696
|
INVALID_PARAMETERS: "Les paramètres donnés ne sont pas valides",
|
|
682
697
|
INVALID_REGEX_MATCH: "Le champ {variable} ne correspond pas au modèle attendu",
|
|
683
698
|
INVALID_NAME: "Le nom {variable} n'est pas valide",
|
|
684
|
-
NO_ENV:
|
|
685
|
-
? "veuillez exécuter avec '--env-file=.env'"
|
|
686
|
-
: "veuillez utiliser dotenv",
|
|
699
|
+
NO_ENV: "",
|
|
687
700
|
},
|
|
688
701
|
es: {
|
|
689
702
|
TABLE_EMPTY: "La tabla {variable} está vacía",
|
|
@@ -698,9 +711,7 @@ export const ERROR_MESSAGES = {
|
|
|
698
711
|
INVALID_PARAMETERS: "Los parámetros proporcionados no son válidos",
|
|
699
712
|
INVALID_REGEX_MATCH: "El campo {variable} no coincide con el patrón esperado",
|
|
700
713
|
INVALID_NAME: "El nombre {variable} no es válido",
|
|
701
|
-
NO_ENV:
|
|
702
|
-
? "por favor ejecute con '--env-file=.env'"
|
|
703
|
-
: "por favor use dotenv",
|
|
714
|
+
NO_ENV: "",
|
|
704
715
|
},
|
|
705
716
|
};
|
|
706
717
|
/**
|
|
@@ -712,7 +723,9 @@ export const ERROR_MESSAGES = {
|
|
|
712
723
|
* @returns An `Error` whose `name` is the error code and `message` is translated.
|
|
713
724
|
*/
|
|
714
725
|
export const createError = (language, name, variable) => {
|
|
715
|
-
const errorMessage =
|
|
726
|
+
const errorMessage = name === "NO_ENV"
|
|
727
|
+
? NO_ENV_MESSAGES[language]?.[supportsEnvFileFlag() ? 0 : 1]
|
|
728
|
+
: ERROR_MESSAGES[language]?.[name];
|
|
716
729
|
if (!errorMessage)
|
|
717
730
|
return new Error("ERR");
|
|
718
731
|
const error = new Error(variable
|