๐ฆ MongoDB Schemas (camelCase)
This page defines the MongoDB document structures for each core collection in the AI Wiki Markup App. All field names follow camelCase convention.
๐ Document Collection
Fields
Field Name |
Description |
Type |
_id |
Unique document ID (MongoDB ObjectId) |
ObjectId |
userId |
Reference to the user who owns the doc |
ObjectId |
title |
Document title |
String |
description |
Optional description or subtitle |
String |
tags |
User-defined tags |
[String] |
autoTags |
AI-generated tags |
[String] |
summary |
AI-generated summary |
String |
currentVersionId |
ID of the current version |
ObjectId |
versionHistory |
List of version IDs |
[ObjectId] |
categoryIds |
Linked categories |
[ObjectId] |
commentIds |
Linked comments |
[ObjectId] |
attachmentPaths |
File paths or URLs |
[String] |
createdAt |
Creation date |
Date |
updatedAt |
Last update date |
Date |
Mongoose Schema
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 }
});
// Add indexes for efficient queries
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 });
// Compound indexes for common queries
documentSchema.index({ userId: 1, isPublished: 1, updatedAt: -1 });
documentSchema.index({ userId: 1, createdAt: -1 });
// Update the updatedAt field on save
documentSchema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
export default mongoose.model('Document', documentSchema);
๐งพ Version Collection
Fields
Field Name |
Description |
Type |
_id |
Unique version ID |
ObjectId |
documentId |
Linked document ID |
ObjectId |
markdown |
GitHub-flavored markdown content |
String |
createdBy |
User who created the version |
ObjectId |
reason |
Optional reason for this version |
String |
createdAt |
Timestamp for version creation |
Date |
Mongoose Schema
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 }
});
// Add indexes for version queries
versionSchema.index({ documentId: 1, createdAt: -1 });
versionSchema.index({ createdBy: 1 });
versionSchema.index({ createdAt: -1 });
// Pre-save middleware to 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 Collection
Fields
Field Name |
Description |
Type |
_id |
Unique category ID |
ObjectId |
name |
Display name of the category |
String |
slug |
URL-safe identifier (e.g., "ai-tools") |
String |
description |
Optional description of the category |
String |
parentId |
Reference to parent category (if nested) |
ObjectId |
path |
Full slug path for nesting (e.g., ["ai","chatgpt"]) |
[String] |
depth |
Level in the hierarchy (0 = root) |
Number |
Mongoose Schema
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 }
});
// Add indexes for category queries
categorySchema.index({ slug: 1 }, { unique: true });
categorySchema.index({ parentId: 1 });
categorySchema.index({ depth: 1 });
categorySchema.index({ path: 1 });
// Virtual for full path string
categorySchema.virtual('fullPath').get(function() {
return this.path.join('/');
});
export default mongoose.model('Category', categorySchema);
๐ฌ Comment Collection
Fields
Field Name |
Description |
Type |
_id |
Unique comment ID |
ObjectId |
documentId |
Reference to the associated document |
ObjectId |
versionId |
(Optional) Linked version if comment is version-specific |
ObjectId |
userId |
Reference to the user who created the comment |
ObjectId |
text |
Comment content |
String |
location |
(Optional) Reference marker in markdown (e.g., line or section) |
String |
createdAt |
Timestamp for when the comment was created |
Date |
Mongoose Schema
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 }
});
export default mongoose.model('Comment', commentSchema);
๐ Attachment Collection
Fields
Field Name |
Description |
Type |
_id |
Unique attachment ID |
ObjectId |
documentId |
Reference to the document this attachment belongs to |
ObjectId |
versionId |
(Optional) Reference to a specific version |
ObjectId |
uploadedBy |
User who uploaded the file |
ObjectId |
fileName |
Original file name |
String |
filePath |
File system path or URL to the stored file |
String |
fileType |
MIME type (e.g., "application/pdf") |
String |
size |
File size in bytes |
Number |
createdAt |
Upload timestamp |
Date |
Mongoose Schema
import mongoose from 'mongoose';
const attachmentSchema = new mongoose.Schema({
documentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Document', required: true },
versionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Version' },
uploadedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
fileName: { type: String, required: true },
filePath: { type: String, required: true },
fileType: { type: String },
size: { type: Number },
createdAt: { type: Date, default: Date.now }
});
// Add indexes for better query performance
attachmentSchema.index({ documentId: 1 });
attachmentSchema.index({ uploadedBy: 1 });
attachmentSchema.index({ createdAt: -1 });
export default mongoose.model('Attachment', attachmentSchema);
๐ค User Collection
Fields
Field Name |
Description |
Type |
_id |
Unique user ID (MongoDB ObjectId) |
ObjectId |
firebaseUid |
Firebase Authentication UID |
String |
displayName |
Full name or display name |
String |
email |
User's email address |
String |
role |
User role ("user" or "admin" ) |
String |
profileImage |
Optional URL or path to profile image |
String |
createdAt |
Timestamp for when the user account was created |
Date |
lastLogin |
Timestamp of most recent login |
Date |
Mongoose Schema
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 for authentication and queries
userSchema.index({ firebaseUid: 1 }, { unique: true });
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ role: 1 });
userSchema.index({ createdAt: 1 });
// Virtual for user's full 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);
๐ Log Collection
Fields
Field Name |
Description |
Type |
_id |
Unique log entry ID |
ObjectId |
level |
Log level ("info" , "warn" , "error" , "debug" ) |
String |
message |
Summary message or label for the log |
String |
context |
Additional context (e.g., "Auth", "OpenAI", "Upload") |
String |
userId |
(Optional) Reference to the user involved |
ObjectId |
meta |
Additional metadata (stack trace, payloads, etc.) |
Mixed (Object) |
timestamp |
When the log was created |
Date |
Mongoose Schema
import mongoose from 'mongoose';
const logSchema = new mongoose.Schema({
level: { type: String, enum: ['info', 'warn', 'error', 'debug'], required: true },
message: { type: String, required: true },
context: { type: String },
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
meta: { type: mongoose.Schema.Types.Mixed },
timestamp: { type: Date, default: Date.now }
});
// Add indexes for log queries and cleanup
logSchema.index({ level: 1 });
logSchema.index({ timestamp: 1 });
logSchema.index({ context: 1 });
logSchema.index({ userId: 1 });
// TTL index to automatically delete old logs (30 days)
logSchema.index({ timestamp: 1 }, { expireAfterSeconds: 30 * 24 * 60 * 60 });
export default mongoose.model('Log', logSchema);
๐ง AI Suggestion Collection
Fields
Field Name |
Description |
Type |
_id |
Unique suggestion ID |
ObjectId |
documentId |
Reference to the document the suggestion was generated for |
ObjectId |
versionId |
(Optional) Reference to the version the suggestion applies to |
ObjectId |
type |
Type of suggestion ("tag" , "summary" , "title" , etc.) |
String |
suggestion |
Suggested value or output (e.g., array of tags, summary text) |
Mixed |
modelUsed |
AI model used (e.g., "gpt-4" ) |
String |
promptUsed |
Prompt sent to the AI for this suggestion |
String |
createdBy |
Source: "system" (automatic) or "user" (manual rerun) |
String |
createdAt |
Timestamp when suggestion was created |
Date |
Mongoose Schema
import mongoose from 'mongoose';
const aiSuggestionSchema = new mongoose.Schema({
documentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Document', required: true },
versionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Version' },
type: { type: String, enum: ['tag', 'summary', 'title'], required: true },
suggestion: { type: mongoose.Schema.Types.Mixed, required: true },
modelUsed: { type: String },
promptUsed: { type: String },
createdBy: { type: String, enum: ['system', 'user'], default: 'system' },
createdAt: { type: Date, default: Date.now }
});
// Add indexes for efficient queries
aiSuggestionSchema.index({ documentId: 1 });
aiSuggestionSchema.index({ type: 1 });
aiSuggestionSchema.index({ createdAt: -1 });
// Compound index for document-specific suggestions
aiSuggestionSchema.index({ documentId: 1, type: 1, createdAt: -1 });
export default mongoose.model('AiSuggestion', aiSuggestionSchema);
๐งโ๐คโ๐ง Access Control Collection
Used to manage shared document access between users. Each entry defines the level of permission a specific user has on a specific document.
Fields
Field Name |
Description |
Type |
_id |
Unique access control entry ID |
ObjectId |
documentId |
ID of the shared document |
ObjectId |
userId |
ID of the user being granted access |
ObjectId |
permission |
Access level ("viewer" or "editor" ) |
String |
grantedBy |
ID of the user (typically owner or admin) who granted access |
ObjectId |
createdAt |
Timestamp when access was granted |
Date |
Mongoose Schema
import mongoose from 'mongoose';
const accessControlSchema = new mongoose.Schema({
documentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Document', required: true },
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
permission: { type: String, enum: ['viewer', 'editor'], required: true },
grantedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
createdAt: { type: Date, default: Date.now }
});
// Compound unique index to prevent duplicate permissions
accessControlSchema.index({ documentId: 1, userId: 1 }, { unique: true });
accessControlSchema.index({ userId: 1 });
accessControlSchema.index({ grantedBy: 1 });
export default mongoose.model('AccessControl', accessControlSchema);
โญ Favorite Collection
Tracks documents that users have marked as favorites for quick access or bookmarking.
Fields
Field Name |
Description |
Type |
_id |
Unique favorite entry ID |
ObjectId |
userId |
ID of the user who favorited |
ObjectId |
documentId |
ID of the favorited document |
ObjectId |
createdAt |
Timestamp when favorite was created |
Date |
Mongoose Schema
import mongoose from 'mongoose';
const favoriteSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
documentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Document', required: true },
createdAt: { type: Date, default: Date.now }
});
// Compound unique index to prevent duplicate favorites
favoriteSchema.index({ userId: 1, documentId: 1 }, { unique: true });
favoriteSchema.index({ userId: 1, createdAt: -1 });
favoriteSchema.index({ documentId: 1 });
export default mongoose.model('Favorite', favoriteSchema);
๐๏ธ Session Collection
Used for managing user sessions and authentication tokens.
Fields
Field Name |
Description |
Type |
_id |
Unique session ID |
ObjectId |
sessionId |
Session identifier token |
String |
userId |
Reference to the user |
ObjectId |
ipAddress |
IP address of the session |
String |
userAgent |
Browser/client user agent |
String |
isActive |
Whether session is currently active |
Boolean |
expiresAt |
Session expiration timestamp |
Date |
createdAt |
Session creation timestamp |
Date |
lastActivity |
Last activity timestamp |
Date |
Mongoose Schema
import mongoose from 'mongoose';
const sessionSchema = new mongoose.Schema({
sessionId: { type: String, required: true, unique: true },
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
ipAddress: { type: String },
userAgent: { type: String },
isActive: { type: Boolean, default: true },
expiresAt: { type: Date, required: true },
createdAt: { type: Date, default: Date.now },
lastActivity: { type: Date, default: Date.now }
});
// TTL index for automatic session cleanup
sessionSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
sessionSchema.index({ sessionId: 1 }, { unique: true });
sessionSchema.index({ userId: 1 });
sessionSchema.index({ isActive: 1 });
export default mongoose.model('Session', sessionSchema);
๐ Analytics Collection
Used for tracking user activity and document analytics.
Fields
Field Name |
Description |
Type |
_id |
Unique analytics entry ID |
ObjectId |
userId |
Reference to the user (if logged in) |
ObjectId |
documentId |
Reference to the document (if applicable) |
ObjectId |
action |
Action type (view, edit, create, etc.) |
String |
metadata |
Additional context data |
Object |
ipAddress |
User's IP address |
String |
userAgent |
Browser/client user agent |
String |
timestamp |
When the action occurred |
Date |
Mongoose Schema
import mongoose from 'mongoose';
const analyticsSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
documentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Document' },
action: {
type: String,
enum: ['view', 'edit', 'create', 'delete', 'share', 'favorite', 'unfavorite', 'comment'],
required: true
},
metadata: { type: mongoose.Schema.Types.Mixed },
ipAddress: { type: String },
userAgent: { type: String },
timestamp: { type: Date, default: Date.now }
});
// Indexes for analytics queries
analyticsSchema.index({ timestamp: -1 });
analyticsSchema.index({ action: 1, timestamp: -1 });
analyticsSchema.index({ userId: 1, timestamp: -1 });
analyticsSchema.index({ documentId: 1, timestamp: -1 });
// TTL index to automatically delete old analytics data (90 days)
analyticsSchema.index({ timestamp: 1 }, { expireAfterSeconds: 90 * 24 * 60 * 60 });
export default mongoose.model('Analytics', analyticsSchema);
๐ง Schema Utilities
Common Query Patterns
Find Documents with User Information
const documentsWithUsers = await Document
.find({ isPublished: true })
.populate('userId', 'displayName email')
.select('title description tags createdAt')
.sort({ createdAt: -1 })
.limit(10);
Get Document with Full Version History
const documentWithHistory = await Document
.findById(documentId)
.populate({
path: 'versionHistory',
select: 'createdAt createdBy reason wordCount',
populate: { path: 'createdBy', select: 'displayName' },
options: { sort: { createdAt: -1 }, limit: 5 }
});
Full-Text Search Across Documents
const searchResults = await Document
.find({ $text: { $search: searchQuery } })
.select('title description tags score')
.sort({ score: { $meta: 'textScore' }, updatedAt: -1 });
Performance Considerations
- Use Indexes: All schemas include appropriate indexes for common query patterns
- Limit Population: Only populate fields you need
- Use Lean Queries: Add
.lean()
for read-only operations
- Implement Pagination: Use
skip()
and limit()
for large datasets
- TTL Indexes: Automatic cleanup for logs, sessions, and analytics
Data Validation
All schemas include:
- Required field validation
- Enum constraints for limited-value fields
- Unique indexes where appropriate
- Pre-save hooks for data processing
- Virtual fields for computed values
Migration Scripts
When updating schemas, create migration scripts:
// Example migration script
import mongoose from 'mongoose';
import Document from './models/Document.js';
async function addPublishedFields() {
await Document.updateMany(
{ isPublished: { $exists: false } },
{
$set: {
isPublished: false,
viewCount: 0
}
}
);
console.log('Migration completed: Added published fields');
}