@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
JavaScript
;
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;