From 1edea83e380d95885e4146b07380867a71156c86 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 29 Jan 2026 16:07:58 -0600 Subject: [PATCH] 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 --- .../migration.sql | 158 +++++++++ apps/api/prisma/schema.prisma | 168 +++++++++- apps/api/prisma/seed.ts | 305 ++++++++++++++++++ 3 files changed, 618 insertions(+), 13 deletions(-) create mode 100644 apps/api/prisma/migrations/20260129220645_add_knowledge_module/migration.sql diff --git a/apps/api/prisma/migrations/20260129220645_add_knowledge_module/migration.sql b/apps/api/prisma/migrations/20260129220645_add_knowledge_module/migration.sql new file mode 100644 index 0000000..72d89bc --- /dev/null +++ b/apps/api/prisma/migrations/20260129220645_add_knowledge_module/migration.sql @@ -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; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index ba85432..854c741 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -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") +} diff --git a/apps/api/prisma/seed.ts b/apps/api/prisma/seed.ts index 35d0010..2e0c501 100644 --- a/apps/api/prisma/seed.ts +++ b/apps/api/prisma/seed.ts @@ -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(); + + 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!"); }