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:

  1. Create Mongoose Models - Define schemas and validation
  2. Set Up Indexes - Optimize query performance
  3. Initialize Collections - Ensure proper structure from the start
  4. 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:

User Model (server/models/User.js)

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);

Document Model (server/models/Document.js)

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);

Version Model (server/models/Version.js)

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);

Category Model (server/models/Category.js)

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:

Comment Model (server/models/Comment.js)

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:

3. Create Models Index File (server/models/index.js)

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
};

4. Initialize Collections Script (scripts/init-database.js)

#!/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();

5. Database Seeding Script (scripts/seed-database.js)

#!/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:

  1. Go to MongoDB Atlas Dashboard
  2. Browse Collections
  3. You'll see all your collections:
    • users
    • documents
    • versions
    • categories
    • comments
    • attachments
    • logs
    • aisuggestions
    • accesscontrols
    • favorites
    • sessions
    • analytics

Best Practices

  1. Always use Mongoose models - Don't insert raw objects
  2. Create indexes early - Before inserting large amounts of data
  3. Use transactions - For operations that affect multiple collections
  4. Validate data - Use Mongoose validation and custom validators
  5. Monitor performance - Use MongoDB Atlas monitoring tools

This approach ensures your MongoDB collections are properly structured with appropriate indexes and validation from the start!