nuvira-parser
Version:
Nuvira Database. New Database format (Readable & Easy to use), (Inbuilt Schema & constraints & rules & relations).
405 lines (340 loc) • 15.7 kB
text/typescript
import { AllowedTypes } from '../types/general';
import { ParsedValueResult } from '../types/records';
export class SQONValidation {
lines: string[];
position: number;
validations: Record<string, any>;
errors: Array<{ line: number; message: string }>;
parsedSchema: Record<string, any>;
validationKeywords: Record<string, string[]>;
constructor({
lines,
position = 0,
parsedSchema,
validationKeywords,
}: {
lines: string[];
position?: number;
parsedSchema: Record<string, any>;
validationKeywords: Record<string, string[]>;
}) {
this.lines = lines;
this.position = position;
this.validations = {};
this.errors = [];
this.parsedSchema = parsedSchema;
this.validationKeywords = validationKeywords;
}
parseValidation(): { validations: Record<string, any>; errors: Array<{ line: number; message: string }> , position: number } {
while (this.position < this.lines.length) {
const line = this.lines[this.position].trim();
if (line === "@end") {
break;
}
if (line.startsWith('!#')) {
this.position++;
continue;
}
if (line.includes("->")) {
this.processValidationLine(line);
} else if (line !== '') {
this.errors.push({ line: this.position + 1, message: `Invalid validation line: "${line}"` });
}
this.position++;
}
return { validations: this.validations, position: this.position, errors: this.errors };
}
processValidationLine(line: string): void {
if (!line.includes("->")) {
this.errors.push({ line: this.position + 1, message: `Invalid format: ${line}` });
return;
}
const [key, rulesStr] = line.split("->").map(s => s.trim());
const rules = this.parseRules(rulesStr);
this.validateRulesAgainstSchema(key, rules);
if (!this.errors.some(err => err.line === this.position + 1)) {
this.addValidation(key, rules);
}
}
addValidation(key: string, rules: Record<string, any>): void {
const parts = key.split('.');
let current = this.validations;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const isLastPart = i === parts.length - 1;
if (isLastPart) {
if (!current[part]) {
current[part] = { rules: {} };
}
current[part].rules = { ...current[part].rules, ...rules };
} else {
if (!current[part]) {
current[part] = {};
}
current = current[part];
}
}
}
parseRules(rulesStr: string): Record<string, any> {
const rules: Record<string, any> = {};
rulesStr.split(';').forEach(rule => {
const [ruleName, ruleValue] = rule.split('=').map(s => s.trim());
if (ruleValue === 'true' || ruleValue === 'false') {
rules[ruleName] = ruleValue === 'true';
} else if (ruleValue === 'null') {
rules[ruleName] = null;
} else if (ruleValue === 'undefined') {
rules[ruleName] = undefined;
} else if (/^\d+$/.test(ruleValue)) {
rules[ruleName] = parseInt(ruleValue, 10);
} else if (/^\d+\.\d+$/.test(ruleValue)) {
rules[ruleName] = parseFloat(ruleValue);
} else if (ruleValue.startsWith('"') && ruleValue.endsWith('"')) {
rules[ruleName] = ruleValue.slice(1, -1);
} else if (ruleValue.startsWith('[') && ruleValue.endsWith(']')) {
const arrayContent = ruleValue.slice(1, -1).trim();
rules[ruleName] = arrayContent
? this.parseArray(arrayContent).map(item => this.parseValue(item))
: [];
} else if (ruleValue.startsWith('{') && ruleValue.endsWith('}')) {
const objectContent = ruleValue.slice(1, -1).trim();
rules[ruleName] = objectContent
? Object.fromEntries(
Object.entries(this.parseObject(objectContent)).map(([key, value]) => [
key,
this.parseValue(value),
])
)
: {};
} else {
this.errors.push({
line: this.position + 1,
message: `Invalid format for value of '${ruleName}' in '${rule}'`
});
}
});
return rules;
}
parseArray(content: string): string[] {
const result: string[] = [];
const items = content.split(',');
for (let i = 0; i < items.length; i++) {
result.push(items[i].trim());
}
return result;
}
parseObject(content: string): Record<string, any> {
const obj: Record<string, any> = {};
const pairs = content.split(',').map(item => item.trim());
pairs.forEach(pair => {
const [key, value] = pair.split(':').map(p => p.trim());
const parsedKey = key.startsWith('"') && key.endsWith('"') ? key.slice(1, -1) : key;
obj[parsedKey] = this.parseValue(value);
});
return obj;
}
validateRulesAgainstSchema(key: string, rules: Record<string, any>): void {
const schemaTypeArray = this.getSchemaType(key);
if (!schemaTypeArray) {
this.errors.push({
line: this.position + 1,
message: `Schema type not found for '${key}'`
});
return;
}
Object.keys(rules).forEach(ruleName => {
const validTypes = this.validationKeywords[ruleName];
const ruleValue = rules[ruleName];
if (!validTypes) {
this.errors.push({
line: this.position + 1,
message: `Validation rule '${ruleName}' is not defined.`
});
return;
}
const isApplicable = validTypes.includes('Any') || schemaTypeArray.some(type =>
validTypes.includes(type as AllowedTypes));
if (!isApplicable) {
this.errors.push({
line: this.position + 1,
message: `Validation '${ruleName}' is not applicable to types for key '${key}'`
});
}
if (ruleName === 'enum' || ruleName === 'hasProperties') {
const isValueValid = schemaTypeArray.some(type => {
if ((type === 'ObjectArray' || type === 'Object[]' || type === 'Object') && Array.isArray(ruleValue)) {
const extractedValues = ruleValue.map((item: any) => item.value);
return extractedValues.every(value => ruleValue.some((allowedValue: any) => allowedValue.value === value)) || ruleValue.length === 0;
}
if (type === 'NumberArray' || type === 'Number[]' && Array.isArray(ruleValue)) {
const extractedValues = ruleValue.map((item: any) => item.value); return extractedValues.every((value: number[]) => ruleValue.some((allowedValue: any) => allowedValue.value === value)) || ruleValue.length === 0;
}
if (type === 'StringArray' || type === 'String[]' && Array.isArray(ruleValue)) {
const extractedValues = ruleValue.map((item: any) => item.value);
return extractedValues.every((value: string[]) => ruleValue.includes(value));
}
return false;
});
if (!isValueValid) {
this.errors.push({
line: this.position + 1,
message: `Invalid value for '${ruleName}' in '${key}'.`
});
}
}
if (schemaTypeArray.includes('ObjectArray')) {
this.validateObjectArrayItems(key, rules);
} else if (schemaTypeArray.includes('Object')) {
this.validateObjectProperties(key, rules);
}
});
}
validateObjectArrayItems(key: string, rules: Record<string, any>): void {
const parts = key.split('.');
const arrayKey = parts[parts.length - 2];
const schemaItem = this.parsedSchema[arrayKey]?.items;
if (!schemaItem) return;
Object.keys(rules).forEach(ruleName => {
const validTypes = this.validationKeywords[ruleName];
if (validTypes && !validTypes.includes('Object')) {
this.errors.push({
line: this.position + 1,
message: `Validation '${ruleName}' is not applicable to type 'ObjectArray' items for key '${key}'`
});
}
});
}
validateObjectProperties(key: string, rules: Record<string, any>): void {
const parts = key.split('.');
const objectKey = parts[parts.length - 2];
const schemaProperties = this.parsedSchema[objectKey]?.properties;
if (!schemaProperties) return;
Object.keys(rules).forEach(ruleName => {
const validTypes = this.validationKeywords[ruleName];
const propSchema = schemaProperties[parts[parts.length - 1]];
if (propSchema && validTypes) {
const propTypes = propSchema.type;
const isValid = propTypes.some((type: any) => validTypes.includes(type) || validTypes.includes('Any'));
if (!isValid) {
this.errors.push({
line: this.position + 1,
message: `Validation '${ruleName}' is not applicable to property '${parts[parts.length - 1]}' of Object for key '${key}'`
});
}
}
});
}
parseRuleValue(value: string): any {
if (value === "true") return true;
if (value === "false") return false;
if (!isNaN(Number(value))) return Number(value);
return value;
}
getSchemaType(key: string): string[] {
const schema = this.parsedSchema;
const parts = key.split('.');
let current = schema;
const processPart = (current: any, parts: string[], index: number): string[] => {
const part = parts[index];
const isLastPart = index === parts.length - 1;
if (isLastPart) {
if (current[part]) {
return Array.isArray(current[part].type) ? current[part].type : [current[part].type];
} else {
return [];
}
}
if (current[part]) {
if (Array.isArray(current[part].type) && current[part].type.includes("Object") && current[part].properties) {
return processPart(current[part].properties, parts, index + 1);
} else if (Array.isArray(current[part].type) && current[part].type.includes("ObjectArray") && current[part].items) {
return processPart(current[part].items, parts, index + 1);
} else {
return [];
}
} else {
return [];
}
};
return processPart(current, parts, 0);
}
parseValue(value: string): ParsedValueResult {
let valueToStore: any;
let type: string;
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 };
}
isValidDate(value: string): boolean {
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());
}
parseDate(value: string): Date | null {
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;
}
}