MongoDB Collection Setup Guide - pacificnm/wiki-ai GitHub Wiki
Creating MongoDB Collections and Models
In MongoDB, collections (equivalent to SQL tables) are created automatically when you first insert a document. However, for a production application, you should create proper Mongoose models and indexes.
Overview
Unlike SQL databases, MongoDB collections are created dynamically. However, you should:
- Create Mongoose Models - Define schemas and validation
- Set Up Indexes - Optimize query performance
- Initialize Collections - Ensure proper structure from the start
- Seed Initial Data - Add default data if needed
Step-by-Step Setup
1. Create Model Files
First, create the model files in your server/models/
directory:
server/models/User.js
)
User Model (import mongoose from 'mongoose';
const userSchema = new mongoose.Schema({
firebaseUid: { type: String, required: true, unique: true },
displayName: { type: String },
email: { type: String, required: true, unique: true },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
profileImage: { type: String },
createdAt: { type: Date, default: Date.now },
lastLogin: { type: Date }
});
// Add indexes
userSchema.index({ firebaseUid: 1 }, { unique: true });
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ role: 1 });
userSchema.index({ createdAt: 1 });
// Virtual for user profile
userSchema.virtual('profile').get(function() {
return {
id: this._id,
displayName: this.displayName,
email: this.email,
role: this.role,
profileImage: this.profileImage
};
});
export default mongoose.model('User', userSchema);
server/models/Document.js
)
Document Model (import mongoose from 'mongoose';
const documentSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
title: { type: String, required: true },
description: { type: String },
tags: [String],
autoTags: [String],
summary: { type: String },
currentVersionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Version' },
versionHistory: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Version' }],
categoryIds: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Category' }],
commentIds: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }],
attachmentPaths: [String],
isPublished: { type: Boolean, default: false },
publishedAt: { type: Date },
viewCount: { type: Number, default: 0 },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
// Indexes
documentSchema.index({ userId: 1 });
documentSchema.index({ title: 'text', description: 'text', tags: 'text' });
documentSchema.index({ createdAt: -1 });
documentSchema.index({ updatedAt: -1 });
documentSchema.index({ isPublished: 1, publishedAt: -1 });
documentSchema.index({ tags: 1 });
documentSchema.index({ categoryIds: 1 });
documentSchema.index({ userId: 1, isPublished: 1, updatedAt: -1 });
documentSchema.index({ userId: 1, createdAt: -1 });
// Update timestamp on save
documentSchema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
export default mongoose.model('Document', documentSchema);
server/models/Version.js
)
Version Model (import mongoose from 'mongoose';
const versionSchema = new mongoose.Schema({
documentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Document', required: true },
markdown: { type: String, required: true },
createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
reason: { type: String },
wordCount: { type: Number },
charCount: { type: Number },
isMinor: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now }
});
// Indexes
versionSchema.index({ documentId: 1, createdAt: -1 });
versionSchema.index({ createdBy: 1 });
versionSchema.index({ createdAt: -1 });
// Calculate word and character counts
versionSchema.pre('save', function(next) {
if (this.markdown) {
this.charCount = this.markdown.length;
this.wordCount = this.markdown.split(/\s+/).filter(word => word.length > 0).length;
}
next();
});
export default mongoose.model('Version', versionSchema);
server/models/Category.js
)
Category Model (import mongoose from 'mongoose';
const categorySchema = new mongoose.Schema({
name: { type: String, required: true },
slug: { type: String, required: true, unique: true },
description: { type: String },
parentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Category' },
path: [{ type: String }],
depth: { type: Number, default: 0 }
});
// Indexes
categorySchema.index({ slug: 1 }, { unique: true });
categorySchema.index({ parentId: 1 });
categorySchema.index({ depth: 1 });
categorySchema.index({ path: 1 });
// Virtual for full path
categorySchema.virtual('fullPath').get(function() {
return this.path.join('/');
});
export default mongoose.model('Category', categorySchema);
2. Create All Models Directory Structure
mkdir -p server/models
Create all the remaining model files:
server/models/Comment.js
)
Comment Model (import mongoose from 'mongoose';
const commentSchema = new mongoose.Schema({
documentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Document', required: true },
versionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Version' },
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
text: { type: String, required: true },
location: { type: String },
createdAt: { type: Date, default: Date.now }
});
commentSchema.index({ documentId: 1, createdAt: -1 });
commentSchema.index({ userId: 1 });
export default mongoose.model('Comment', commentSchema);
Other Models (Attachment, Log, AiSuggestion, AccessControl, Favorite, Session, Analytics)
I'll create these as separate files, but here's a consolidated models index file:
server/models/index.js
)
3. Create Models Index File (import User from './User.js';
import Document from './Document.js';
import Version from './Version.js';
import Category from './Category.js';
import Comment from './Comment.js';
import Attachment from './Attachment.js';
import Log from './Log.js';
import AiSuggestion from './AiSuggestion.js';
import AccessControl from './AccessControl.js';
import Favorite from './Favorite.js';
import Session from './Session.js';
import Analytics from './Analytics.js';
export {
User,
Document,
Version,
Category,
Comment,
Attachment,
Log,
AiSuggestion,
AccessControl,
Favorite,
Session,
Analytics
};
scripts/init-database.js
)
4. Initialize Collections Script (#!/usr/bin/env node
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import { connectToDatabase, initializeDatabase } from '../server/config/database.js';
import { logger } from '../server/middleware/logger.js';
// Import all models to ensure they're registered
import {
User,
Document,
Version,
Category,
Comment,
Attachment,
Log,
AiSuggestion,
AccessControl,
Favorite,
Session,
Analytics
} from '../server/models/index.js';
dotenv.config();
/**
* Initialize database with collections and seed data.
*/
async function initDatabase() {
try {
logger.info('Starting database initialization...');
// Connect to database
await connectToDatabase();
// Initialize collections and indexes
await initializeDatabase();
// Create default categories
await createDefaultCategories();
// Create admin user (if doesn't exist)
await createAdminUser();
logger.info('Database initialization completed successfully!');
process.exit(0);
} catch (error) {
logger.error('Database initialization failed', { error: error.message });
process.exit(1);
}
}
/**
* Create default categories.
*/
async function createDefaultCategories() {
try {
const defaultCategories = [
{
name: 'General',
slug: 'general',
description: 'General documents and notes',
depth: 0,
path: ['general']
},
{
name: 'Technical',
slug: 'technical',
description: 'Technical documentation',
depth: 0,
path: ['technical']
},
{
name: 'Tutorials',
slug: 'tutorials',
description: 'How-to guides and tutorials',
depth: 0,
path: ['tutorials']
},
{
name: 'API Documentation',
slug: 'api-docs',
description: 'API reference and documentation',
depth: 0,
path: ['api-docs']
}
];
for (const categoryData of defaultCategories) {
const existing = await Category.findOne({ slug: categoryData.slug });
if (!existing) {
const category = new Category(categoryData);
await category.save();
logger.info(`Created default category: ${category.name}`);
}
}
} catch (error) {
logger.warn('Failed to create default categories', { error: error.message });
}
}
/**
* Create default admin user.
*/
async function createAdminUser() {
try {
// Check if admin user already exists
const adminExists = await User.findOne({ role: 'admin' });
if (adminExists) {
logger.info('Admin user already exists');
return;
}
// Create admin user (you'll need to set this up with Firebase first)
const adminUser = new User({
firebaseUid: 'admin-firebase-uid', // Replace with actual Firebase UID
displayName: 'Admin User',
email: '[email protected]', // Replace with your admin email
role: 'admin'
});
await adminUser.save();
logger.info('Created admin user');
} catch (error) {
logger.warn('Failed to create admin user', { error: error.message });
}
}
// Run initialization
initDatabase();
scripts/seed-database.js
)
5. Database Seeding Script (#!/usr/bin/env node
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import { connectToDatabase } from '../server/config/database.js';
import { logger } from '../server/middleware/logger.js';
import { User, Document, Version, Category } from '../server/models/index.js';
dotenv.config();
/**
* Seed database with sample data.
*/
async function seedDatabase() {
try {
logger.info('Starting database seeding...');
await connectToDatabase();
// Create sample user
const user = await createSampleUser();
// Create sample categories
const categories = await createSampleCategories();
// Create sample documents
await createSampleDocuments(user, categories);
logger.info('Database seeding completed successfully!');
process.exit(0);
} catch (error) {
logger.error('Database seeding failed', { error: error.message });
process.exit(1);
}
}
async function createSampleUser() {
const userData = {
firebaseUid: 'sample-user-123',
displayName: 'Sample User',
email: '[email protected]',
role: 'user'
};
let user = await User.findOne({ email: userData.email });
if (!user) {
user = new User(userData);
await user.save();
logger.info('Created sample user');
}
return user;
}
async function createSampleCategories() {
const categoryData = [
{ name: 'Getting Started', slug: 'getting-started', description: 'Introduction guides' },
{ name: 'Development', slug: 'development', description: 'Development documentation' }
];
const categories = [];
for (const data of categoryData) {
let category = await Category.findOne({ slug: data.slug });
if (!category) {
category = new Category(data);
await category.save();
logger.info(`Created sample category: ${category.name}`);
}
categories.push(category);
}
return categories;
}
async function createSampleDocuments(user, categories) {
const documents = [
{
title: 'Welcome to Wiki AI',
description: 'Getting started with the wiki system',
markdown: '# Welcome\n\nThis is your first document!',
tags: ['welcome', 'intro'],
categoryIds: [categories[0]._id]
},
{
title: 'API Documentation',
description: 'Complete API reference',
markdown: '# API Docs\n\n## Authentication\n\nUse Firebase tokens...',
tags: ['api', 'docs'],
categoryIds: [categories[1]._id]
}
];
for (const docData of documents) {
const existing = await Document.findOne({
title: docData.title,
userId: user._id
});
if (!existing) {
// Create document
const document = new Document({
...docData,
userId: user._id,
isPublished: true,
publishedAt: new Date()
});
await document.save();
// Create initial version
const version = new Version({
documentId: document._id,
markdown: docData.markdown,
createdBy: user._id,
reason: 'Initial version'
});
await version.save();
// Update document with version
document.currentVersionId = version._id;
document.versionHistory = [version._id];
await document.save();
logger.info(`Created sample document: ${document.title}`);
}
}
}
// Run seeding
seedDatabase();
6. Package.json Scripts
Add these scripts to your package.json
:
{
"scripts": {
"db:init": "node scripts/init-database.js",
"db:seed": "node scripts/seed-database.js",
"db:reset": "node scripts/reset-database.js"
}
}
7. Usage Commands
# Initialize database (create collections and indexes)
npm run db:init
# Seed with sample data
npm run db:seed
# Reset database (be careful!)
npm run db:reset
How Collections Are Created
Automatic Creation
When you first save a document using a Mongoose model:
const user = new User({
firebaseUid: 'user123',
email: '[email protected]',
displayName: 'John Doe'
});
await user.save(); // This creates the 'users' collection automatically
Manual Collection Creation
You can also create collections explicitly:
// Create collection with validation
await mongoose.connection.db.createCollection('users', {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["email", "firebaseUid"],
properties: {
email: { bsonType: "string" },
firebaseUid: { bsonType: "string" }
}
}
}
});
Indexes Creation
Automatic Index Creation (Development)
Set this in your database config for development:
mongoose.set('autoIndex', true); // Only for development
Manual Index Creation (Production)
For production, create indexes manually:
// In your initialization script
await User.createIndexes();
await Document.createIndexes();
await Version.createIndexes();
// ... for all models
MongoDB Atlas Collections View
After running the initialization:
- Go to MongoDB Atlas Dashboard
- Browse Collections
- You'll see all your collections:
users
documents
versions
categories
comments
attachments
logs
aisuggestions
accesscontrols
favorites
sessions
analytics
Best Practices
- Always use Mongoose models - Don't insert raw objects
- Create indexes early - Before inserting large amounts of data
- Use transactions - For operations that affect multiple collections
- Validate data - Use Mongoose validation and custom validators
- Monitor performance - Use MongoDB Atlas monitoring tools
This approach ensures your MongoDB collections are properly structured with appropriate indexes and validation from the start!