UNPKG

node-mongo-admin

Version:

A simple web application to visualize mongo data inspired by PHPMyAdmin

240 lines (214 loc) 6.01 kB
'use strict' let mongodb = require('mongodb'), /** @var {Object<{date: new Date, num: number, schema: Object}>} */ cache = {} /** * @typedef {Object<Sample~SchemaField>} Sample~Schema */ /** * @typedef {Object} Sample~SchemaField * @property {?boolean|Array<number>} double - up to 10 values * @property {?boolean|Array<string>} string - up to 10 values * @property {?boolean} object * @property {?boolean} array * @property {?boolean} binData * @property {?boolean} objectId * @property {?boolean} bool * @property {?boolean} date * @property {?boolean} null * @property {?boolean} regex * @property {?boolean} timestamp * @property {?boolean} long * @property {?boolean} minKey * @property {?boolean} maxKey */ /** * @typedef {Object<Sample~SchemaNode>} Sample~SchemaNode * @property {?Object<Sample~SchemaNode>} children - children by field name */ module.exports.fields = { connection: String, collection: String } module.exports.handler = function (dbs, body, success, error) { let db = dbs[body.connection], schema = {}, flatSchema = Object.create(null), num = 0, answered = false, timer if (!db) { return error(200, 'Invalid connection name') } let cacheKey = body.connection + '.' + body.collection, cached = cache[cacheKey] if (cached && cached.date.getTime() > Date.now() - 3600e3) { // Cache still fresh return success({ num: cached.num, schema: cached.schema }) } // Use up to 5s to analyze all data timer = setTimeout(answer, 10e3) flatSchema[''] = {} // Sample 1000 docs from the collection and process them // as they come let cursor = db.collection(body.collection).aggregate([{ $sample: { size: 1e3 } }]) cursor.each((err, doc) => { if (answered) { return } else if (err || !doc) { return answer() } num += 1 processObj(doc, schema, '', flatSchema) }) function answer() { if (answered) { return } answered = true delete flatSchema[''] clearTimeout(timer) // eslint-disable-next-line callback-return success({ num, schema: flatSchema }) cursor.close() cache[cacheKey] = { date: new Date, num, schema: flatSchema } } } /** * Process a type 'object' value * @param {Object} doc * @param {Sample~SchemaNode} schema * @param {string} field * @param {Sample~Schema} flatSchema */ function processObj(obj, schema, field, flatSchema) { let hadChildren = true, fieldSchema = flatSchema[field] fieldSchema.object = true // Look for missing fields if (!schema.children) { schema.children = Object.create(null) hadChildren = false } else { for (let key in schema.children) { if (!(key in obj)) { let childField = field ? field + '.' + key : key flatSchema[childField].null = true } } } // Walk fields for (let key in obj) { let childSchema = schema.children[key], childField = field ? field + '.' + key : key, childFieldSchema = flatSchema[childField] || (flatSchema[childField] = {}) if (!childSchema) { childSchema = schema.children[key] = {} childFieldSchema.null = !hadChildren ? childFieldSchema.null : true } processValue(obj[key], childSchema, childField, flatSchema) } } /** * Process a type 'array' value * @param {Array<*>} arr * @param {Sample~SchemaNode} schema * @param {string} field * @param {Sample~Schema} flatSchema */ function processArray(arr, schema, field, flatSchema) { let fieldSchema = flatSchema[field] fieldSchema.array = true // Look for missing fields if (!schema.children) { schema.children = Object.create(null) } else { for (let key in schema.children) { let childField = field ? field + '.' + key : key flatSchema[childField].null = true } } // Walk values arr.forEach(element => { processValue(element, schema, field, flatSchema) }) } /** * @param {*} value * @param {Sample~SchemaNode} schema * @param {string} field * @param {Sample~Schema} flatSchema */ function processValue(value, schema, field, flatSchema) { let fieldSchema = flatSchema[field] if (typeof value === 'number') { addToEnum(fieldSchema, 'double', value) } else if (typeof value === 'string') { addToEnum(fieldSchema, 'string', value) } else if (value === null) { fieldSchema.null = true } else if (value instanceof Date) { fieldSchema.date = true } else if (value instanceof RegExp) { fieldSchema.regex = true } else if (typeof value === 'boolean') { fieldSchema.bool = true } else if (value instanceof mongodb.ObjectId) { fieldSchema.objectId = true } else if (value instanceof mongodb.Binary) { fieldSchema.binData = true } else if (value instanceof mongodb.Timestamp) { fieldSchema.timestamp = true } else if (value instanceof mongodb.Long) { fieldSchema.long = true } else if (value instanceof mongodb.MinKey) { fieldSchema.minKey = true } else if (value instanceof mongodb.MaxKey) { fieldSchema.maxKey = true } else if (Array.isArray(value)) { processArray(value, schema, field, flatSchema) } else if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) { processObj(value, schema, field, flatSchema) } } /** * Handle value enumerations (up to 10) * @param {Sample~SchemaField} fieldSchema - modified in-place * @param {string} typeName * @param {number|string} value */ function addToEnum(fieldSchema, typeName, value) { let type = fieldSchema[typeName] if (type === true) { // No longer an enum return } else if (typeof value === 'string' && value.length > 50) { // Too large fieldSchema[typeName] = true return } else if (!type) { type = fieldSchema[typeName] = [] } if (type.indexOf(value) === -1) { type.push(value) if (type.length > 10) { // Too many values fieldSchema[typeName] = true } } }