UNPKG

@dataql/node

Version:

DataQL core SDK for unified data management with MongoDB and GraphQL - Production Multi-Cloud Ready

216 lines (215 loc) 7.85 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DatabaseIntrospector = void 0; /** * Base class for database introspection */ class DatabaseIntrospector { constructor(connection, options = {}) { this.connection = connection; this.options = { sampleSize: options.sampleSize || 100, maxDepth: options.maxDepth || 5, includeIndexes: options.includeIndexes !== false, excludeCollections: options.excludeCollections || [], includeCollections: options.includeCollections, ...options, }; } /** * Analyze field type from sample values */ analyzeFieldType(values, fieldName) { const nonNullValues = values.filter((v) => v !== null && v !== undefined); const totalValues = values.length; const nonNullCount = nonNullValues.length; if (nonNullCount === 0) { return { type: "String", nullable: true }; } // Check if field is nullable const nullable = nonNullCount < totalValues; // Get unique types const types = new Set(nonNullValues.map((v) => this.getValueType(v))); // If all values are the same type, use that type if (types.size === 1) { const primaryType = Array.from(types)[0]; const analysis = { type: primaryType, nullable }; // Special analysis for specific types if (primaryType === "String") { analysis.format = this.detectStringFormat(nonNullValues); // Check if it's an enum (small set of repeated values) const uniqueValues = new Set(nonNullValues); if (uniqueValues.size <= 10 && uniqueValues.size < nonNullCount * 0.8) { analysis.enum = Array.from(uniqueValues); } } else if (primaryType === "Object") { // Analyze nested object structure const nestedFields = this.analyzeNestedObject(nonNullValues); if (Object.keys(nestedFields).length > 0) { analysis.nested = nestedFields; } } else if (primaryType === "Array") { // Analyze array items const allItems = nonNullValues.flat(); if (allItems.length > 0) { analysis.items = this.analyzeFieldType(allItems, `${fieldName}_item`); } } // Add examples (up to 3 unique values) const uniqueValues = Array.from(new Set(nonNullValues.slice(0, 10))); analysis.examples = uniqueValues.slice(0, 3); return analysis; } // Mixed types - use String as fallback but note the variety return { type: "String", nullable, examples: Array.from(new Set(nonNullValues.slice(0, 3))), }; } /** * Get DataQL type for a JavaScript value */ getValueType(value) { if (value === null || value === undefined) return "String"; if (typeof value === "string") { // Check for ObjectId pattern if (/^[0-9a-fA-F]{24}$/.test(value)) return "ID"; // Check for ISO date pattern if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) return "Timestamp"; return "String"; } if (typeof value === "number") { return Number.isInteger(value) ? "Int" : "Number"; } if (typeof value === "boolean") return "Boolean"; if (value instanceof Date) return "Timestamp"; if (Array.isArray(value)) return "Array"; if (typeof value === "object") return "Object"; return "String"; } /** * Detect string format (email, URL, etc.) */ detectStringFormat(values) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const urlRegex = /^https?:\/\/.+/; const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/; let emailCount = 0; let urlCount = 0; let phoneCount = 0; for (const value of values.slice(0, 20)) { // Check first 20 values if (typeof value === "string") { if (emailRegex.test(value)) emailCount++; if (urlRegex.test(value)) urlCount++; if (phoneRegex.test(value)) phoneCount++; } } const total = Math.min(values.length, 20); if (emailCount / total > 0.8) return "email"; if (urlCount / total > 0.8) return "url"; if (phoneCount / total > 0.8) return "phone"; return undefined; } /** * Analyze nested object structure */ analyzeNestedObject(objects) { const fieldMap = {}; // Collect all field values for (const obj of objects) { if (obj && typeof obj === "object" && !Array.isArray(obj)) { for (const [key, value] of Object.entries(obj)) { if (!fieldMap[key]) fieldMap[key] = []; fieldMap[key].push(value); } } } // Analyze each field const result = {}; for (const [fieldName, values] of Object.entries(fieldMap)) { result[fieldName] = this.analyzeFieldType(values, fieldName); } return result; } /** * Convert introspection results to DataQL schemas */ generateDataQLSchemas(collections) { const schemas = {}; for (const collection of collections) { schemas[collection.name] = this.convertFieldsToSchema(collection.fields); } return schemas; } /** * Convert field analysis to DataQL schema format */ convertFieldsToSchema(fields) { const schema = {}; for (const [fieldName, analysis] of Object.entries(fields)) { schema[fieldName] = this.convertFieldAnalysisToSchemaField(analysis); } return schema; } /** * Convert single field analysis to DataQL schema field */ convertFieldAnalysisToSchemaField(analysis) { let field = { type: analysis.type }; if (analysis.required) field.required = true; if (analysis.enum) field.enum = analysis.enum; if (analysis.format) field.format = analysis.format; if (analysis.type === "Object" && analysis.nested) { field = { type: "object", properties: this.convertFieldsToSchema(analysis.nested), }; } else if (analysis.type === "Array" && analysis.items) { field = { type: "array", items: this.convertFieldAnalysisToSchemaField(analysis.items), }; } return field; } /** * Filter collections based on include/exclude options */ filterCollections(collections) { let filtered = collections; // Apply include filter first if (this.options.includeCollections && this.options.includeCollections.length > 0) { filtered = filtered.filter((name) => this.options.includeCollections.includes(name)); } // Apply exclude filter if (this.options.excludeCollections && this.options.excludeCollections.length > 0) { filtered = filtered.filter((name) => !this.options.excludeCollections.includes(name)); } return filtered; } } exports.DatabaseIntrospector = DatabaseIntrospector;