nuvira
Version:
Nuvira Database. New Database format (Readable & Easy to use), (Inbuilt Schema & constraints & rules & relations).
505 lines • 20.5 kB
JavaScript
export class NuviraRecords {
lines;
position;
records;
errors;
expectedDocNumber = 0;
/**
* Constructs the NuviraRecords object.
* @param lines - The input lines to parse.
* @param position - The starting position to begin parsing. Defaults to 0.
*/
constructor(lines, position = 0) {
this.lines = lines;
this.position = position;
this.records = [];
this.errors = [];
}
/**
* Parses records from the provided lines.
* @param batchSize - The maximum number of lines to process at once. Defaults to 10.
* @returns An object containing the parsed records, the updated position, the current line, and errors (if any).
*/
parseRecords(batchSize = 10) {
const startMarker = '@records';
const endMarker = '@end';
if (!this.lines[this.position - 1].startsWith(startMarker)) {
this.errors.push({ line: this.position, message: `Records must start with '${startMarker}'.` });
return {
records: [],
position: this.position,
errors: this.errors
};
}
let batch = [];
while (this.position < this.lines.length) {
const line = this.lines[this.position];
if (line === endMarker) {
if (batch.length > 0) {
this.processBatch(batch);
}
this.position++;
break;
}
if (line.startsWith('!#')) {
this.position++;
continue;
}
batch.push(line);
if (batch.length >= batchSize) {
this.processBatch(batch);
batch = [];
}
this.position++;
}
return {
records: this.records,
position: this.position,
errors: this.errors
};
}
/**
* Processes a batch of lines.
* @param batch - The lines to process.
*/
processBatch(batch) {
const cleanedBatch = this.cleanBatchContent(batch);
cleanedBatch.forEach((line, index) => {
const match = line.match(/^#(\d+)\s*->\s*(.+)$/);
if (match) {
const docNumber = parseInt(match[1], 10);
const docContent = match[2];
if (docNumber !== this.expectedDocNumber) {
this.errors.push({
line: this.position + index + 1,
message: `Document number should be ${this.expectedDocNumber}, found ${docNumber}.`
});
}
this.expectedDocNumber++;
const keyValuePairs = this.parseDocumentContent(docContent, docNumber, this.position + index + 1);
this.records.push({ '#doc': docNumber, data: keyValuePairs });
}
else {
this.errors.push({
line: this.position + index + 1,
message: `Invalid document line: '${line}' in Document #${this.expectedDocNumber}`
});
}
});
}
/**
* Cleans and formats the batch content.
* @param batch - The batch of lines to clean.
* @returns An array of cleaned lines.
*/
cleanBatchContent(batch) {
const result = [];
let combinedContent = [];
batch.forEach(line => {
if (line.startsWith("#") && combinedContent.length > 0) {
result.push(combinedContent.join(" ").trim());
combinedContent = [];
}
combinedContent.push(line);
});
if (combinedContent.length > 0)
result.push(combinedContent.join(" ").trim());
return result;
}
/**
* Parses the content of a document and extracts key-value pairs.
* @param docContent - The content of the document.
* @param docNumber - The document number.
* @param lineNumber - The line number where the document is located.
* @returns An array of key-value pairs.
*/
parseDocumentContent(docContent, docNumber, lineNumber) {
const keyValuePairs = [];
const kvPattern = /(\w+)\(([^)]*)\);|(\w+)\{|(\w+)\[/g;
let kvMatch;
let lastIndex = 0;
while ((kvMatch = kvPattern.exec(docContent)) !== null) {
const precedingContent = docContent.slice(lastIndex, kvMatch.index).trim();
if (precedingContent.length > 0 && !precedingContent.endsWith(';')) {
this.errors.push({ line: lineNumber, message: this.logError(`Document #${docNumber}: Missing semicolon before key '${kvMatch[1] || kvMatch[3] || kvMatch[4]}'`) });
}
lastIndex = kvPattern.lastIndex;
let key, valueToStore, type;
if (kvMatch[1]) {
key = kvMatch[1];
const value = kvMatch[2].trim();
valueToStore = this.parseValue(value);
if ('error' in valueToStore) {
this.errors.push({ line: lineNumber, message: this.logError(valueToStore.error) });
continue;
}
type = valueToStore.type;
keyValuePairs.push({ key, value: valueToStore.value, type });
}
else if (kvMatch[3]) {
key = kvMatch[3];
const objectContent = this.extractObjectContent(docContent, lastIndex);
const objectKeyValuePairs = objectContent === '{}' ? {} : this.parseObjectContent(objectContent, docNumber, lineNumber);
keyValuePairs.push({ key, value: objectKeyValuePairs, type: 'Object' });
lastIndex += objectContent.length + 2;
kvPattern.lastIndex = lastIndex;
}
else if (kvMatch[4]) {
key = kvMatch[4];
const arrayContent = this.extractArrayContent(docContent, lastIndex);
const { arrayItems, arrayType } = arrayContent === '[]'
? { arrayItems: [], arrayType: 'Array' }
: this.parseArrayContent(arrayContent, docNumber, lineNumber);
keyValuePairs.push({ key, value: arrayItems, type: arrayType });
lastIndex += arrayContent.length + 2;
kvPattern.lastIndex = lastIndex;
}
}
if (!docContent.trim().endsWith(';')) {
this.errors.push({ line: lineNumber, message: this.logError(`Document #${docNumber}: Missing semicolon at the end of the document.`) });
}
return keyValuePairs;
}
/**
* Parses the content of a document and extracts key-value pairs.
* @param docContent - The content of the document.
* @param docNumber - The document number.
* @param lineNumber - The line number where the document is located.
* @returns An array of key-value pairs.
*/
parseArrayContent(arrayContent, docNumber, lineNumber) {
const arrayItems = [];
const itemTypes = new Set();
const kvPattern = /([A-Za-z_$][A-Za-z0-9_$-]*)\[([^\]]*)\](?:\s*;)?|([A-Za-z_$][A-Za-z0-9_$-]*)\(([^)]*)\);|([A-Za-z_$][A-Za-z0-9_$-]*)\{/g;
let kvMatch;
let lastIndex = 0;
while ((kvMatch = kvPattern.exec(arrayContent)) !== null) {
const precedingContent = arrayContent.slice(lastIndex, kvMatch.index).trim();
const currentKey = kvMatch[1] || kvMatch[3] || kvMatch[5];
if (!this.isValidKeyName(currentKey)) {
this.errors.push({
line: lineNumber,
message: this.logError(`Invalid key name '${currentKey}' in document #${docNumber}.`)
});
continue;
}
if (precedingContent.length > 0 && !precedingContent.endsWith(';')) {
this.errors.push({
line: lineNumber,
message: this.logError(`Document #${docNumber}: Missing semicolon before key '${currentKey}'`)
});
}
lastIndex = kvPattern.lastIndex;
if (kvMatch[1] && kvMatch[2]) {
const key = kvMatch[1];
const arrayValues = kvMatch[2].trim();
if (arrayValues) {
const values = arrayValues.split(',').map(v => {
const parsed = parseFloat(v.trim());
return isNaN(parsed) ? null : parsed;
}).filter(v => v !== null);
if (values.length > 0) {
arrayItems.push({
key,
value: values,
type: 'NumberArray'
});
itemTypes.add('NumberArray');
}
}
}
else if (kvMatch[3] && kvMatch[4]) {
const key = kvMatch[3];
const value = kvMatch[4].trim();
const parsedValue = this.parseValue(value);
if ('error' in parsedValue) {
this.errors.push({
line: lineNumber,
message: this.logError(parsedValue.error)
});
continue;
}
arrayItems.push({
key,
value: parsedValue.value,
type: parsedValue.type
});
itemTypes.add(parsedValue.type);
}
else if (kvMatch[5]) {
const key = kvMatch[5];
const nestedObjectContent = this.extractObjectContent(arrayContent, kvPattern.lastIndex);
const nestedObjectKeyValuePairs = this.parseObjectContent(nestedObjectContent, docNumber, lineNumber);
arrayItems.push({
key,
value: nestedObjectKeyValuePairs,
type: 'Object'
});
itemTypes.add('Object');
lastIndex += nestedObjectContent.length + 2;
kvPattern.lastIndex = lastIndex;
}
}
const arrayType = this.determineArrayType(itemTypes);
return { arrayItems, arrayType };
}
/**
* Determines the array type based on the types of items in the array.
* @param itemTypes - A set of types of the items within the array.
* @returns A string representing the type of the array (e.g., 'NumberArray', 'StringArray', etc.).
*/
determineArrayType(itemTypes) {
const types = Array.from(itemTypes);
if (types.length === 0)
return 'Array';
if (types.every(type => type === 'Number')) {
return 'NumberArray';
}
if (types.every(type => type === 'String')) {
return 'StringArray';
}
if (types.every(type => type === 'Object')) {
return 'ObjectArray';
}
return 'AnyArray';
}
/**
* Parses the content of an object to extract key-value pairs.
* @param objectContent - The content of the object as a string.
* @param docNumber - The document number for error tracking.
* @param lineNumber - The line number in the document where the object is located.
* @returns An array of key-value pairs parsed from the object content.
*/
parseObjectContent(objectContent, docNumber, lineNumber) {
if (objectContent === '{}') {
return [];
}
const objectKeyValuePairs = [];
const kvPattern = /(\w+)\(([^)]*)\);|(\w+)\{|(\w+)\[/g;
let kvMatch;
let lastIndex = 0;
while ((kvMatch = kvPattern.exec(objectContent)) !== null) {
const precedingContent = objectContent.slice(lastIndex, kvMatch.index).trim();
const currentKey = kvMatch[1] || kvMatch[3] || kvMatch[4];
if (precedingContent.length > 0 && !precedingContent.endsWith(';')) {
this.errors.push({
line: lineNumber,
message: this.logError(`Document #${docNumber}: Missing semicolon before key '${currentKey}'`)
});
}
lastIndex = kvPattern.lastIndex;
let key, valueToStore, type;
if (kvMatch[1]) {
key = kvMatch[1];
const value = kvMatch[2].trim();
valueToStore = this.parseValue(value);
type = valueToStore.type;
objectKeyValuePairs.push({ key, value: valueToStore.value, type });
}
else if (kvMatch[3]) {
key = kvMatch[3];
const nestedObjectContent = this.extractObjectContent(objectContent, kvPattern.lastIndex);
const nestedObjectKeyValuePairs = nestedObjectContent === '{}' ? {} : this.parseObjectContent(nestedObjectContent, docNumber, lineNumber);
objectKeyValuePairs.push({ key, value: nestedObjectKeyValuePairs, type: 'Object' });
lastIndex += nestedObjectContent.length + 2;
kvPattern.lastIndex = lastIndex;
}
else if (kvMatch[4]) {
key = kvMatch[4];
const arrayContentInner = this.extractArrayContent(objectContent, kvPattern.lastIndex);
const { arrayItems: innerArrayItems, arrayType } = arrayContentInner === '[]'
? { arrayItems: [], arrayType: 'array' }
: this.parseArrayContent(arrayContentInner, docNumber, lineNumber);
objectKeyValuePairs.push({ key, value: innerArrayItems, type: arrayType });
lastIndex += arrayContentInner.length + 2;
kvPattern.lastIndex = lastIndex;
}
}
return objectKeyValuePairs;
}
/**
* Extracts the content of an object from a string, handling nested braces.
* @param content - The content of the string to extract from.
* @param startIndex - The starting index where the object content begins.
* @returns A string containing the object content.
*/
extractObjectContent(content, startIndex) {
let i = startIndex;
let depth = 1;
let extracted = "";
while (i < content.length && depth > 0) {
const char = content[i];
if (char === "{")
depth++;
if (char === "}")
depth--;
if (depth > 0)
extracted += char;
i++;
}
return depth === 0 ? extracted.trim() : "{}";
}
/**
* Extracts the content of an array from a string, handling nested brackets.
* @param content - The content of the string to extract from.
* @param startIndex - The starting index where the array content begins.
* @returns A string containing the array content.
*/
extractArrayContent(content, startIndex) {
let i = startIndex;
let depth = 1;
let extracted = "";
while (i < content.length && depth > 0) {
const char = content[i];
if (char === "[")
depth++;
if (char === "]")
depth--;
if (depth > 0)
extracted += char;
i++;
}
return depth === 0 ? extracted.trim() : "[]";
}
/**
* Parses a value from a string and determines its type.
* @param value - The value as a string to parse.
* @returns An object containing the parsed value and its type.
*/
parseValue(value) {
let valueToStore;
let type;
if (value === "") {
valueToStore = undefined;
type = 'undefined';
}
else if (value.startsWith('"') && value.endsWith('"')) {
valueToStore = value.slice(1, -1);
type = valueToStore === "" ? 'undefined' : 'String';
valueToStore = valueToStore === "" ? undefined : valueToStore;
}
else if (value.startsWith('<Buffer') && value.endsWith('>')) {
valueToStore = value;
type = 'Binary';
}
else if (value.startsWith('Uint8Array[') && value.endsWith(']')) {
valueToStore = value;
type = 'Uint8Array';
}
else if (!isNaN(Number(value)) && !isNaN(parseFloat(value)) && !value.startsWith('0x')) {
const numberValue = parseFloat(value);
if (numberValue > Number.MAX_SAFE_INTEGER || numberValue < Number.MIN_SAFE_INTEGER) {
valueToStore = BigInt(value);
type = 'Number';
}
else {
valueToStore = numberValue;
type = 'Number';
}
}
else if (value === 'TRUE') {
valueToStore = true;
type = 'Boolean';
}
else if (value === 'FALSE') {
valueToStore = false;
type = 'Boolean';
}
else if (value === 'NULL') {
valueToStore = null;
type = 'Null';
}
else if (value === 'undefined') {
valueToStore = undefined;
type = 'undefined';
}
else if (this.isValidDate(value)) {
valueToStore = this.parseDate(value);
type = 'Date';
if (!valueToStore) {
return { error: `Invalid date format: '${value}'.`, type: 'error' };
}
}
else {
return { error: `Invalid value format: '${value}'.`, type: 'error' };
}
return { value: valueToStore, type };
}
/**
* Checks if a string value is a valid date format.
* @param value - The string value to check.
* @returns True if the value is a valid date format, false otherwise.
*/
isValidDate(value) {
const dateFormats = [
/^\d{1,2}(st|nd|rd|th)?\s+\w+\s+\d{4}$/i,
/^\d{1,2}[/-]\d{1,2}[/-]\d{2,4}$/,
/^\d{4}[/-]\d{1,2}[/-]\d{1,2}$/,
/^\d{1,2}:\d{2}:\d{2}([AP]M)?$/,
/^\d{1,2}:\d{2}([AP]M)?$/,
/^\d{10}$/,
/^\d{13}$/,
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|([+-]\d{2}:\d{2}))?$/
];
const matchesRegex = dateFormats.some(format => format.test(value));
if (matchesRegex) {
return true;
}
const parsedDate = new Date(value);
return !isNaN(parsedDate.getTime());
}
/**
* Parses a date string into a Date object.
* @param value - The string representing a date to parse.
* @returns A Date object or null if the date is invalid.
*/
parseDate(value) {
const dayMonthYear = value.match(/^(\d{1,2})(st|nd|rd|th)?\s+(\w+)\s+(\d{4})$/);
if (dayMonthYear) {
const day = parseInt(dayMonthYear[1], 10);
const month = new Date(`${dayMonthYear[3]} 1`).getMonth();
const year = parseInt(dayMonthYear[4], 10);
return new Date(year, month, day);
}
if (!isNaN(Date.parse(value))) {
return new Date(value);
}
return null;
}
/**
* Validates a key name based on specific criteria.
* @param key - The key name to validate.
* @returns True if the key name is valid, false otherwise.
*/
isValidKeyName(key) {
const keyPattern = /^[A-Za-z0-9_$-]+$/;
const containsLetterOrSpecialChar = /[A-Za-z_$-]/.test(key);
const startsWithValidCharacter = /^[A-Za-z_$]/.test(key);
return key.length > 0 && keyPattern.test(key) && containsLetterOrSpecialChar && startsWithValidCharacter;
}
/**
* Logs an error message.
* @param message - The error message to log.
* @returns A formatted error message string.
*/
logError(message) {
return `ERR: ${message}`;
}
/**
* Logs a warning message.
* @param message - The warning message to log.
* @returns A formatted warning message string.
*/
logWarn(message) {
return `WARN: ${message}`;
}
/**
* Logs an informational message.
* @param message - The informational message to log.
* @returns A formatted info message string.
*/
logInfo(message) {
return `INFO: ${message}`;
}
}
//# sourceMappingURL=parseRecords.js.map