feat(knowledge): add database schema for Knowledge Module

Implements KNOW-001
- KnowledgeEntry, Version, Link, Tag, Embedding models
- Full indexes and constraints
- Seed data for testing
This commit is contained in:
Jason Woltje
2026-01-29 16:07:58 -06:00
parent 9b7e1f926f
commit 1edea83e38
3 changed files with 618 additions and 13 deletions

View File

@@ -0,0 +1,158 @@
-- CreateEnum
CREATE TYPE "EntryStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');
-- CreateEnum
CREATE TYPE "Visibility" AS ENUM ('PRIVATE', 'WORKSPACE', 'PUBLIC');
-- AlterTable
ALTER TABLE "user_layouts" ADD COLUMN "metadata" JSONB NOT NULL DEFAULT '{}';
-- CreateTable
CREATE TABLE "knowledge_entries" (
"id" UUID NOT NULL,
"workspace_id" UUID NOT NULL,
"slug" TEXT NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"content_html" TEXT,
"summary" TEXT,
"status" "EntryStatus" NOT NULL DEFAULT 'DRAFT',
"visibility" "Visibility" NOT NULL DEFAULT 'PRIVATE',
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL,
"created_by" UUID NOT NULL,
"updated_by" UUID NOT NULL,
CONSTRAINT "knowledge_entries_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "knowledge_entry_versions" (
"id" UUID NOT NULL,
"entry_id" UUID NOT NULL,
"version" INTEGER NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"summary" TEXT,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"created_by" UUID NOT NULL,
"change_note" TEXT,
CONSTRAINT "knowledge_entry_versions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "knowledge_links" (
"id" UUID NOT NULL,
"source_id" UUID NOT NULL,
"target_id" UUID NOT NULL,
"link_text" TEXT NOT NULL,
"context" TEXT,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "knowledge_links_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "knowledge_tags" (
"id" UUID NOT NULL,
"workspace_id" UUID NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"color" TEXT,
"description" TEXT,
CONSTRAINT "knowledge_tags_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "knowledge_entry_tags" (
"entry_id" UUID NOT NULL,
"tag_id" UUID NOT NULL,
CONSTRAINT "knowledge_entry_tags_pkey" PRIMARY KEY ("entry_id","tag_id")
);
-- CreateTable
CREATE TABLE "knowledge_embeddings" (
"id" UUID NOT NULL,
"entry_id" UUID NOT NULL,
"embedding" vector(1536) NOT NULL,
"model" TEXT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL,
CONSTRAINT "knowledge_embeddings_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "knowledge_entries_workspace_id_status_idx" ON "knowledge_entries"("workspace_id", "status");
-- CreateIndex
CREATE INDEX "knowledge_entries_workspace_id_updated_at_idx" ON "knowledge_entries"("workspace_id", "updated_at");
-- CreateIndex
CREATE INDEX "knowledge_entries_created_by_idx" ON "knowledge_entries"("created_by");
-- CreateIndex
CREATE INDEX "knowledge_entries_updated_by_idx" ON "knowledge_entries"("updated_by");
-- CreateIndex
CREATE UNIQUE INDEX "knowledge_entries_workspace_id_slug_key" ON "knowledge_entries"("workspace_id", "slug");
-- CreateIndex
CREATE INDEX "knowledge_entry_versions_entry_id_version_idx" ON "knowledge_entry_versions"("entry_id", "version");
-- CreateIndex
CREATE UNIQUE INDEX "knowledge_entry_versions_entry_id_version_key" ON "knowledge_entry_versions"("entry_id", "version");
-- CreateIndex
CREATE INDEX "knowledge_links_source_id_idx" ON "knowledge_links"("source_id");
-- CreateIndex
CREATE INDEX "knowledge_links_target_id_idx" ON "knowledge_links"("target_id");
-- CreateIndex
CREATE UNIQUE INDEX "knowledge_links_source_id_target_id_key" ON "knowledge_links"("source_id", "target_id");
-- CreateIndex
CREATE INDEX "knowledge_tags_workspace_id_idx" ON "knowledge_tags"("workspace_id");
-- CreateIndex
CREATE UNIQUE INDEX "knowledge_tags_workspace_id_slug_key" ON "knowledge_tags"("workspace_id", "slug");
-- CreateIndex
CREATE INDEX "knowledge_entry_tags_entry_id_idx" ON "knowledge_entry_tags"("entry_id");
-- CreateIndex
CREATE INDEX "knowledge_entry_tags_tag_id_idx" ON "knowledge_entry_tags"("tag_id");
-- CreateIndex
CREATE UNIQUE INDEX "knowledge_embeddings_entry_id_key" ON "knowledge_embeddings"("entry_id");
-- CreateIndex
CREATE INDEX "knowledge_embeddings_entry_id_idx" ON "knowledge_embeddings"("entry_id");
-- AddForeignKey
ALTER TABLE "knowledge_entries" ADD CONSTRAINT "knowledge_entries_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "knowledge_entry_versions" ADD CONSTRAINT "knowledge_entry_versions_entry_id_fkey" FOREIGN KEY ("entry_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "knowledge_links" ADD CONSTRAINT "knowledge_links_source_id_fkey" FOREIGN KEY ("source_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "knowledge_links" ADD CONSTRAINT "knowledge_links_target_id_fkey" FOREIGN KEY ("target_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "knowledge_tags" ADD CONSTRAINT "knowledge_tags_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "knowledge_entry_tags" ADD CONSTRAINT "knowledge_entry_tags_entry_id_fkey" FOREIGN KEY ("entry_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "knowledge_entry_tags" ADD CONSTRAINT "knowledge_entry_tags_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "knowledge_tags"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "knowledge_embeddings" ADD CONSTRAINT "knowledge_embeddings_entry_id_fkey" FOREIGN KEY ("entry_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -96,6 +96,18 @@ enum AgentStatus {
TERMINATED
}
enum EntryStatus {
DRAFT
PUBLISHED
ARCHIVED
}
enum Visibility {
PRIVATE
WORKSPACE
PUBLIC
}
// ============================================
// MODELS
// ============================================
@@ -138,19 +150,21 @@ model Workspace {
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
// Relations
owner User @relation("WorkspaceOwner", fields: [ownerId], references: [id], onDelete: Cascade)
members WorkspaceMember[]
tasks Task[]
events Event[]
projects Project[]
activityLogs ActivityLog[]
memoryEmbeddings MemoryEmbedding[]
domains Domain[]
ideas Idea[]
relationships Relationship[]
agents Agent[]
agentSessions AgentSession[]
userLayouts UserLayout[]
owner User @relation("WorkspaceOwner", fields: [ownerId], references: [id], onDelete: Cascade)
members WorkspaceMember[]
tasks Task[]
events Event[]
projects Project[]
activityLogs ActivityLog[]
memoryEmbeddings MemoryEmbedding[]
domains Domain[]
ideas Idea[]
relationships Relationship[]
agents Agent[]
agentSessions AgentSession[]
userLayouts UserLayout[]
knowledgeEntries KnowledgeEntry[]
knowledgeTags KnowledgeTag[]
@@index([ownerId])
@@map("workspaces")
@@ -605,3 +619,131 @@ model Verification {
@@index([identifier])
@@map("verifications")
}
// ============================================
// KNOWLEDGE MODULE
// ============================================
model KnowledgeEntry {
id String @id @default(uuid()) @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
// Identity
slug String
title String
// Content
content String @db.Text
contentHtml String? @map("content_html") @db.Text
summary String?
// Status
status EntryStatus @default(DRAFT)
visibility Visibility @default(PRIVATE)
// Audit
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
createdBy String @map("created_by") @db.Uuid
updatedBy String @map("updated_by") @db.Uuid
// Relations
tags KnowledgeEntryTag[]
outgoingLinks KnowledgeLink[] @relation("SourceEntry")
incomingLinks KnowledgeLink[] @relation("TargetEntry")
versions KnowledgeEntryVersion[]
embedding KnowledgeEmbedding?
@@unique([workspaceId, slug])
@@index([workspaceId, status])
@@index([workspaceId, updatedAt])
@@index([createdBy])
@@index([updatedBy])
@@map("knowledge_entries")
}
model KnowledgeEntryVersion {
id String @id @default(uuid()) @db.Uuid
entryId String @map("entry_id") @db.Uuid
entry KnowledgeEntry @relation(fields: [entryId], references: [id], onDelete: Cascade)
version Int
title String
content String @db.Text
summary String?
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
createdBy String @map("created_by") @db.Uuid
changeNote String? @map("change_note")
@@unique([entryId, version])
@@index([entryId, version])
@@map("knowledge_entry_versions")
}
model KnowledgeLink {
id String @id @default(uuid()) @db.Uuid
sourceId String @map("source_id") @db.Uuid
source KnowledgeEntry @relation("SourceEntry", fields: [sourceId], references: [id], onDelete: Cascade)
targetId String @map("target_id") @db.Uuid
target KnowledgeEntry @relation("TargetEntry", fields: [targetId], references: [id], onDelete: Cascade)
// Link metadata
linkText String @map("link_text")
context String?
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
@@unique([sourceId, targetId])
@@index([sourceId])
@@index([targetId])
@@map("knowledge_links")
}
model KnowledgeTag {
id String @id @default(uuid()) @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
name String
slug String
color String?
description String?
entries KnowledgeEntryTag[]
@@unique([workspaceId, slug])
@@index([workspaceId])
@@map("knowledge_tags")
}
model KnowledgeEntryTag {
entryId String @map("entry_id") @db.Uuid
entry KnowledgeEntry @relation(fields: [entryId], references: [id], onDelete: Cascade)
tagId String @map("tag_id") @db.Uuid
tag KnowledgeTag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([entryId, tagId])
@@index([entryId])
@@index([tagId])
@@map("knowledge_entry_tags")
}
model KnowledgeEmbedding {
id String @id @default(uuid()) @db.Uuid
entryId String @unique @map("entry_id") @db.Uuid
entry KnowledgeEntry @relation(fields: [entryId], references: [id], onDelete: Cascade)
embedding Unsupported("vector(1536)")
model String
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
@@index([entryId])
@@map("knowledge_embeddings")
}

View File

@@ -4,6 +4,8 @@ import {
TaskPriority,
ProjectStatus,
WorkspaceMemberRole,
EntryStatus,
Visibility,
} from "@prisma/client";
const prisma = new PrismaClient();
@@ -145,6 +147,309 @@ async function main() {
});
console.log("Created sample event");
// ============================================
// KNOWLEDGE MODULE SEED DATA
// ============================================
// Delete existing knowledge data
await tx.knowledgeEmbedding.deleteMany({ where: { entry: { workspaceId: workspace.id } } });
await tx.knowledgeEntryTag.deleteMany({ where: { entry: { workspaceId: workspace.id } } });
await tx.knowledgeLink.deleteMany({ where: { source: { workspaceId: workspace.id } } });
await tx.knowledgeEntryVersion.deleteMany({ where: { entry: { workspaceId: workspace.id } } });
await tx.knowledgeEntry.deleteMany({ where: { workspaceId: workspace.id } });
await tx.knowledgeTag.deleteMany({ where: { workspaceId: workspace.id } });
// Create knowledge tags
const tags = await Promise.all([
tx.knowledgeTag.create({
data: {
workspaceId: workspace.id,
name: "Architecture",
slug: "architecture",
color: "#3B82F6",
description: "System architecture and design decisions",
},
}),
tx.knowledgeTag.create({
data: {
workspaceId: workspace.id,
name: "Development",
slug: "development",
color: "#10B981",
description: "Development practices and guidelines",
},
}),
tx.knowledgeTag.create({
data: {
workspaceId: workspace.id,
name: "Getting Started",
slug: "getting-started",
color: "#F59E0B",
description: "Onboarding and setup guides",
},
}),
]);
console.log(`Created ${tags.length} knowledge tags`);
// Create knowledge entries
const entries = [
{
slug: "welcome",
title: "Welcome to Mosaic Stack Knowledge Base",
content: `# Welcome to Mosaic Stack
This is the knowledge base for the Mosaic Stack project. Here you'll find:
- **[[architecture-overview]]** - High-level system architecture
- **[[development-setup]]** - Getting started with development
- **[[database-schema]]** - Database design and conventions
## About This Knowledge Base
The Knowledge Module provides:
- Wiki-style linking between entries
- Full-text and semantic search
- Version history and change tracking
- Tag-based organization
Start exploring by following the links above!`,
summary: "Introduction to the Mosaic Stack knowledge base and navigation guide",
status: EntryStatus.PUBLISHED,
visibility: Visibility.WORKSPACE,
tags: ["getting-started"],
},
{
slug: "architecture-overview",
title: "Architecture Overview",
content: `# Architecture Overview
The Mosaic Stack is built on a modern, scalable architecture:
## Stack Components
- **Frontend**: Next.js 15+ with React 19
- **Backend**: NestJS with Prisma ORM
- **Database**: PostgreSQL 17 with pgvector
- **Cache**: Valkey (Redis fork)
## Key Modules
1. **Task Management** - See [[development-setup]] for local setup
2. **Event Calendar** - Integrated scheduling
3. **Knowledge Base** - This module! See [[database-schema]]
4. **Agent Orchestration** - AI agent coordination
The database schema is documented in [[database-schema]].`,
summary: "High-level overview of Mosaic Stack architecture and components",
status: EntryStatus.PUBLISHED,
visibility: Visibility.WORKSPACE,
tags: ["architecture"],
},
{
slug: "development-setup",
title: "Development Setup Guide",
content: `# Development Setup Guide
## Prerequisites
- Node.js 22+
- PostgreSQL 17
- pnpm 9+
## Quick Start
\`\`\`bash
# Clone the repository
git clone https://git.mosaicstack.dev/mosaic/stack.git
# Install dependencies
pnpm install
# Set up database
cd apps/api
pnpm prisma migrate dev
pnpm prisma:seed
# Start development servers
pnpm dev
\`\`\`
## Architecture
Before diving in, review the [[architecture-overview]] to understand the system design.
## Database
The database schema is documented in [[database-schema]]. All models use Prisma ORM.
## Next Steps
1. Read the [[architecture-overview]]
2. Explore the codebase
3. Check out the [[database-schema]] documentation`,
summary: "Step-by-step guide to setting up the Mosaic Stack development environment",
status: EntryStatus.PUBLISHED,
visibility: Visibility.WORKSPACE,
tags: ["development", "getting-started"],
},
{
slug: "database-schema",
title: "Database Schema Documentation",
content: `# Database Schema Documentation
## Overview
Mosaic Stack uses PostgreSQL 17 with Prisma ORM. See [[architecture-overview]] for context.
## Key Conventions
- All IDs are UUIDs
- Timestamps use \`@db.Timestamptz\` (timezone-aware)
- Soft deletes via status fields (e.g., ARCHIVED)
- Relations enforce cascade deletes for data integrity
## Core Models
### Task Management
- \`Task\` - Individual work items
- \`Project\` - Task containers
- \`Domain\` - High-level categorization
### Knowledge Module
- \`KnowledgeEntry\` - Wiki pages
- \`KnowledgeLink\` - Page connections
- \`KnowledgeTag\` - Categorization
- \`KnowledgeEmbedding\` - Semantic search (pgvector)
### Agent System
- \`Agent\` - AI agent instances
- \`AgentSession\` - Conversation sessions
## Migrations
Migrations are managed via Prisma:
\`\`\`bash
# Create migration
pnpm prisma migrate dev --name my_migration
# Apply in production
pnpm prisma migrate deploy
\`\`\`
For setup instructions, see [[development-setup]].`,
summary: "Comprehensive documentation of the Mosaic Stack database schema and Prisma conventions",
status: EntryStatus.PUBLISHED,
visibility: Visibility.WORKSPACE,
tags: ["architecture", "development"],
},
{
slug: "future-ideas",
title: "Future Ideas and Roadmap",
content: `# Future Ideas and Roadmap
## Planned Features
- Real-time collaboration (CRDT)
- Advanced graph visualizations
- AI-powered summarization
- Mobile app
## Research Areas
- Vector search optimization
- Knowledge graph algorithms
- Agent memory systems
This is a draft document. See [[architecture-overview]] for current state.`,
summary: "Brainstorming document for future features and research directions",
status: EntryStatus.DRAFT,
visibility: Visibility.PRIVATE,
tags: [],
},
];
// Create entries and track them for linking
const createdEntries = new Map<string, any>();
for (const entryData of entries) {
const entry = await tx.knowledgeEntry.create({
data: {
workspaceId: workspace.id,
slug: entryData.slug,
title: entryData.title,
content: entryData.content,
summary: entryData.summary,
status: entryData.status,
visibility: entryData.visibility,
createdBy: user.id,
updatedBy: user.id,
},
});
createdEntries.set(entryData.slug, entry);
// Create initial version
await tx.knowledgeEntryVersion.create({
data: {
entryId: entry.id,
version: 1,
title: entry.title,
content: entry.content,
summary: entry.summary,
createdBy: user.id,
changeNote: "Initial version",
},
});
// Add tags
for (const tagSlug of entryData.tags) {
const tag = tags.find(t => t.slug === tagSlug);
if (tag) {
await tx.knowledgeEntryTag.create({
data: {
entryId: entry.id,
tagId: tag.id,
},
});
}
}
}
console.log(`Created ${entries.length} knowledge entries`);
// Create wiki-links between entries
const links = [
{ source: "welcome", target: "architecture-overview", text: "architecture-overview" },
{ source: "welcome", target: "development-setup", text: "development-setup" },
{ source: "welcome", target: "database-schema", text: "database-schema" },
{ source: "architecture-overview", target: "development-setup", text: "development-setup" },
{ source: "architecture-overview", target: "database-schema", text: "database-schema" },
{ source: "development-setup", target: "architecture-overview", text: "architecture-overview" },
{ source: "development-setup", target: "database-schema", text: "database-schema" },
{ source: "database-schema", target: "architecture-overview", text: "architecture-overview" },
{ source: "database-schema", target: "development-setup", text: "development-setup" },
{ source: "future-ideas", target: "architecture-overview", text: "architecture-overview" },
];
for (const link of links) {
const sourceEntry = createdEntries.get(link.source);
const targetEntry = createdEntries.get(link.target);
if (sourceEntry && targetEntry) {
await tx.knowledgeLink.create({
data: {
sourceId: sourceEntry.id,
targetId: targetEntry.id,
linkText: link.text,
},
});
}
}
console.log(`Created ${links.length} knowledge links`);
});
console.log("Seeding completed successfully!");
}