Skip to content

Commit 9bd2485

Browse files
committed
Describe collections, main methods and helpers
1 parent b67f094 commit 9bd2485

File tree

1 file changed

+236
-0
lines changed

1 file changed

+236
-0
lines changed

hacks/describe_collections.js

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
function isObjectLiteral (element) {
2+
return Object.prototype.toString.call(element) === '[object Object]' ||
3+
Object.prototype.toString.call(element) === '[object BSON]';
4+
}
5+
6+
function isObjectLiteralOrArray (element) {
7+
return Array.isArray(element) || isObjectLiteral(element);
8+
}
9+
10+
function isArrayOfObjects (array) {
11+
if (array.length === 0) {
12+
return false;
13+
}
14+
15+
for (let i = 0; i < array.length; i++) {
16+
let element = array[i];
17+
18+
if (!isObjectLiteral(element)) {
19+
return false
20+
}
21+
}
22+
return true;
23+
}
24+
25+
function checkTypesSetNames (element) {
26+
if (typeof element == 'object') {
27+
if (element === null) {
28+
return { hackerColorName: 'null', displayName: 'unknown' }
29+
} else if (element === undefined) {
30+
return { hackerColorName: 'undefined', displayName: 'undefined' }
31+
} else if (element instanceof ObjectId) {
32+
return { hackerColorName: 'objectid', displayName: 'Object Id' }
33+
} else if (element instanceof Date) {
34+
return { hackerColorName: 'date', displayName: 'Date' }
35+
} else if (Array.isArray(element)) {
36+
return { hackerColorName: 'function', displayName: 'Array' }
37+
} else {
38+
if (isObjectLiteral(element)) {
39+
return { hackerColorName: 'function', displayName: 'Object' }
40+
}
41+
42+
return { hackerColorName: 'null', displayName: 'unknown' }
43+
}
44+
} else if (typeof element === 'boolean') {
45+
return { hackerColorName: 'boolean', displayName: 'Boolean' }
46+
} else if (typeof element === 'number') {
47+
return { hackerColorName: 'number', displayName: 'Number' }
48+
} else if (typeof element === 'string') {
49+
return { hackerColorName: 'string', displayName: 'String' }
50+
} else {
51+
return { hackerColorName: 'null', displayName: 'unknown' }
52+
}
53+
}
54+
55+
function printWithIndentation (prop, renderedType, hackerType, levelsDeep) {
56+
const coloredProp = colorize(prop, mongo_hacker_config['colors']['key']);
57+
const coloredType = ': ' +
58+
colorize(
59+
renderedType,
60+
mongo_hacker_config['colors'][hackerType]
61+
);
62+
63+
print(' '.repeat(levelsDeep) + coloredProp + coloredType );
64+
}
65+
66+
/*
67+
* Updates a specific value in an object given an array of keys
68+
* representing the path to it.
69+
*
70+
* @param {object} obj - object to update.
71+
* @param {string[]} breadcrumbs - array of strings representing keys.
72+
* @param {any} newValue - new value to be set.
73+
*/
74+
function breadCrumbUpdate (obj, breadcrumbs, newValue) {
75+
for (let i = 0; i < breadcrumbs.length; i++) {
76+
let key = breadcrumbs[i];
77+
78+
if (i === breadcrumbs.length - 1) {
79+
return obj[key] = newValue;
80+
}
81+
82+
obj = obj[key];
83+
}
84+
}
85+
86+
/*
87+
* Returns a specific value given an array of keys representing
88+
* the path to it.
89+
*
90+
* @param {object} obj - object or document to get the value from.
91+
* @param {string[]} breadcrumbs - array of strings representing keys.
92+
* @return {any}
93+
*/
94+
function getValueByBreadCrumb (obj, breadcrumbs) {
95+
return breadcrumbs.reduce(function (acc, key, index) {
96+
return acc[key]
97+
}, obj);
98+
}
99+
100+
/*
101+
* The purpose of this function is to build the most complete example
102+
* of an object given an array of examples. Similar to Object.assign it keeps
103+
* adding new fields but only overriding if it encounters falsy values
104+
*
105+
* For array of objects it will create just one representation of all
106+
* elements in array.
107+
*
108+
* @param {objects[]} dataArray - Array of plain objects or mongo documents.
109+
* @return {object}
110+
*/
111+
function buildModelDoc (dataArray) {
112+
const finalObject = {};
113+
114+
function traverse (element, breadcrumbs) {
115+
if (Array.isArray(element)) {
116+
if (isArrayOfObjects(element)) {
117+
const mergedObject = buildModelDoc(element);
118+
breadCrumbUpdate(finalObject, breadcrumbs, [mergedObject]);
119+
} else {
120+
breadCrumbUpdate(finalObject, breadcrumbs, element)
121+
}
122+
} else {
123+
Object.getOwnPropertyNames(element).forEach(function (key) {
124+
const value = element[key];
125+
const finalObjectAtLevel = getValueByBreadCrumb(finalObject, breadcrumbs);
126+
127+
if (finalObjectAtLevel[key]) {
128+
if (
129+
finalObjectAtLevel[key] === null ||
130+
finalObjectAtLevel[key] === undefined
131+
) {
132+
breadCrumbUpdate(finalObject, breadcrumbs.concat(key), value);
133+
}
134+
} else {
135+
breadCrumbUpdate(finalObject, breadcrumbs.concat(key), value);
136+
}
137+
138+
if (isObjectLiteralOrArray(value)) {
139+
traverse(value, breadcrumbs.concat(key));
140+
}
141+
});
142+
}
143+
}
144+
145+
dataArray.forEach(function (doc) {
146+
traverse(doc, []);
147+
});
148+
149+
return finalObject;
150+
}
151+
152+
//----------------------------------------------------------------------------
153+
// Collections Descriptions
154+
//----------------------------------------------------------------------------
155+
156+
// Since the nature of mongodb is to have schemaless documents, the best we can
157+
// do is to infer the schema by taking a sample of the total documents in a given
158+
// collection. Sample size can be set and it defaults to 100 documents
159+
// or less if there are not as many.
160+
161+
/*
162+
* Prints infered schema from a sample of docuements in collection.
163+
* Space indentation is used to show deeper levels.
164+
*
165+
* @param {number} maxSamples - number of random documents to infer schema
166+
*/
167+
DBCollection.prototype.describe = function (maxSamples) {
168+
const sampleMaxSize = maxSamples || 100;
169+
const totalDocs = this.count();
170+
171+
if (!totalDocs) {
172+
return print('\nNo documents to infer schema');
173+
}
174+
175+
const randomSampleDocs = this.aggregate([
176+
{
177+
$sample: { size: sampleMaxSize }
178+
}
179+
]);
180+
181+
print(
182+
colorize(
183+
'\nInfered from ' +
184+
randomSampleDocs._batch.length +
185+
' sample documents.',
186+
mongo_hacker_config['colors']['string']
187+
)
188+
);
189+
190+
print(
191+
colorize(
192+
'Some fields may be missing...\n',
193+
mongo_hacker_config['colors']['string']
194+
)
195+
);
196+
197+
const mergedSamples = buildModelDoc(randomSampleDocs);
198+
199+
function traverse (doc, levelsDeep) {
200+
Object.getOwnPropertyNames(doc).forEach(function (prop) {
201+
const value = doc[prop];
202+
const type = checkTypesSetNames(value);
203+
204+
if (Array.isArray(value)) {
205+
if (isArrayOfObjects(value)) {
206+
const mergedObjects = buildModelDoc(value);
207+
const displayName = 'Array of Objects';
208+
printWithIndentation(prop, displayName, 'function', levelsDeep);
209+
traverse(mergedObjects, levelsDeep + 1);
210+
} else {
211+
const typesSet = new Set();
212+
213+
value.forEach(function (element) {
214+
typesSet.add(checkTypesSetNames(element).displayName + 's');
215+
});
216+
217+
const typesArray = Array.from(typesSet);
218+
const displayName = 'Array' +
219+
(typesArray.length ? ( ' of ' + typesArray.join('|') ) : '');
220+
221+
printWithIndentation(prop, displayName, 'function', levelsDeep);
222+
}
223+
} else {
224+
printWithIndentation(prop, type.displayName, type.hackerColorName, levelsDeep);
225+
226+
if (type.displayName === 'Object') {
227+
traverse(value, levelsDeep + 1);
228+
}
229+
}
230+
});
231+
}
232+
233+
traverse(mergedSamples, 1);
234+
print('\n');
235+
}
236+

0 commit comments

Comments
 (0)