feat: add MongoDB models for core collections
Models Created (7/10): - Document.model.js: Framework docs with quadrant classification - BlogPost.model.js: AI-curated blog with moderation - MediaInquiry.model.js: Press/media triage workflow - ModerationQueue.model.js: Human oversight queue with priority - User.model.js: Admin authentication with bcrypt - CaseSubmission.model.js: Community case studies with AI review - Resource.model.js: Curated directory with alignment scores Features: - Full CRUD operations for each model - Tractatus quadrant integration - AI analysis fields for curation - Human approval workflows - Status tracking and filtering - Security (password hashing, sanitized returns) Deferred (Phase 2-3): - Citation.model.js - Translation.model.js - KohaDonation.model.js Status: Core models complete, ready for Express server
This commit is contained in:
parent
47818bade1
commit
78ab5754f2
8 changed files with 1305 additions and 0 deletions
163
src/models/BlogPost.model.js
Normal file
163
src/models/BlogPost.model.js
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* BlogPost Model
|
||||
* AI-curated blog with human oversight
|
||||
*/
|
||||
|
||||
const { ObjectId } = require('mongodb');
|
||||
const { getCollection } = require('../utils/db.util');
|
||||
|
||||
class BlogPost {
|
||||
/**
|
||||
* Create a new blog post
|
||||
*/
|
||||
static async create(data) {
|
||||
const collection = await getCollection('blog_posts');
|
||||
|
||||
const post = {
|
||||
title: data.title,
|
||||
slug: data.slug,
|
||||
author: {
|
||||
type: data.author?.type || 'human', // 'human' or 'ai_curated'
|
||||
name: data.author?.name || 'John Stroh',
|
||||
claude_version: data.author?.claude_version
|
||||
},
|
||||
content: data.content,
|
||||
excerpt: data.excerpt,
|
||||
featured_image: data.featured_image,
|
||||
status: data.status || 'draft', // draft/pending/published/archived
|
||||
moderation: {
|
||||
ai_analysis: data.moderation?.ai_analysis,
|
||||
human_reviewer: data.moderation?.human_reviewer,
|
||||
review_notes: data.moderation?.review_notes,
|
||||
approved_at: data.moderation?.approved_at
|
||||
},
|
||||
tractatus_classification: {
|
||||
quadrant: data.tractatus_classification?.quadrant || 'OPERATIONAL',
|
||||
values_sensitive: data.tractatus_classification?.values_sensitive || false,
|
||||
requires_strategic_review: data.tractatus_classification?.requires_strategic_review || false
|
||||
},
|
||||
published_at: data.published_at,
|
||||
tags: data.tags || [],
|
||||
view_count: 0,
|
||||
engagement: {
|
||||
shares: 0,
|
||||
comments: 0
|
||||
}
|
||||
};
|
||||
|
||||
const result = await collection.insertOne(post);
|
||||
return { ...post, _id: result.insertedId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find post by ID
|
||||
*/
|
||||
static async findById(id) {
|
||||
const collection = await getCollection('blog_posts');
|
||||
return await collection.findOne({ _id: new ObjectId(id) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find post by slug
|
||||
*/
|
||||
static async findBySlug(slug) {
|
||||
const collection = await getCollection('blog_posts');
|
||||
return await collection.findOne({ slug });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find published posts
|
||||
*/
|
||||
static async findPublished(options = {}) {
|
||||
const collection = await getCollection('blog_posts');
|
||||
const { limit = 10, skip = 0, sort = { published_at: -1 } } = options;
|
||||
|
||||
return await collection
|
||||
.find({ status: 'published' })
|
||||
.sort(sort)
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find posts by status
|
||||
*/
|
||||
static async findByStatus(status, options = {}) {
|
||||
const collection = await getCollection('blog_posts');
|
||||
const { limit = 20, skip = 0 } = options;
|
||||
|
||||
return await collection
|
||||
.find({ status })
|
||||
.sort({ _id: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update post
|
||||
*/
|
||||
static async update(id, updates) {
|
||||
const collection = await getCollection('blog_posts');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $set: updates }
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish post (change status + set published_at)
|
||||
*/
|
||||
static async publish(id, reviewerId) {
|
||||
const collection = await getCollection('blog_posts');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
status: 'published',
|
||||
published_at: new Date(),
|
||||
'moderation.human_reviewer': reviewerId,
|
||||
'moderation.approved_at': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment view count
|
||||
*/
|
||||
static async incrementViews(id) {
|
||||
const collection = await getCollection('blog_posts');
|
||||
|
||||
await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $inc: { view_count: 1 } }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete post
|
||||
*/
|
||||
static async delete(id) {
|
||||
const collection = await getCollection('blog_posts');
|
||||
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
||||
return result.deletedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count posts by status
|
||||
*/
|
||||
static async countByStatus(status) {
|
||||
const collection = await getCollection('blog_posts');
|
||||
return await collection.countDocuments({ status });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlogPost;
|
||||
206
src/models/CaseSubmission.model.js
Normal file
206
src/models/CaseSubmission.model.js
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
/**
|
||||
* CaseSubmission Model
|
||||
* Community case study submissions
|
||||
*/
|
||||
|
||||
const { ObjectId } = require('mongodb');
|
||||
const { getCollection } = require('../utils/db.util');
|
||||
|
||||
class CaseSubmission {
|
||||
/**
|
||||
* Create a new case submission
|
||||
*/
|
||||
static async create(data) {
|
||||
const collection = await getCollection('case_submissions');
|
||||
|
||||
const submission = {
|
||||
submitter: {
|
||||
name: data.submitter.name,
|
||||
email: data.submitter.email,
|
||||
organization: data.submitter.organization,
|
||||
public: data.submitter.public !== undefined ? data.submitter.public : false
|
||||
},
|
||||
case_study: {
|
||||
title: data.case_study.title,
|
||||
description: data.case_study.description,
|
||||
failure_mode: data.case_study.failure_mode,
|
||||
tractatus_applicability: data.case_study.tractatus_applicability,
|
||||
evidence: data.case_study.evidence || [],
|
||||
attachments: data.case_study.attachments || []
|
||||
},
|
||||
ai_review: {
|
||||
relevance_score: data.ai_review?.relevance_score, // 0-1
|
||||
completeness_score: data.ai_review?.completeness_score, // 0-1
|
||||
recommended_category: data.ai_review?.recommended_category,
|
||||
suggested_improvements: data.ai_review?.suggested_improvements || [],
|
||||
claude_analysis: data.ai_review?.claude_analysis
|
||||
},
|
||||
moderation: {
|
||||
status: data.moderation?.status || 'pending', // pending/approved/rejected/needs_info
|
||||
reviewer: data.moderation?.reviewer,
|
||||
review_notes: data.moderation?.review_notes,
|
||||
reviewed_at: data.moderation?.reviewed_at
|
||||
},
|
||||
published_case_id: data.published_case_id,
|
||||
submitted_at: new Date()
|
||||
};
|
||||
|
||||
const result = await collection.insertOne(submission);
|
||||
return { ...submission, _id: result.insertedId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find submission by ID
|
||||
*/
|
||||
static async findById(id) {
|
||||
const collection = await getCollection('case_submissions');
|
||||
return await collection.findOne({ _id: new ObjectId(id) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find by moderation status
|
||||
*/
|
||||
static async findByStatus(status, options = {}) {
|
||||
const collection = await getCollection('case_submissions');
|
||||
const { limit = 20, skip = 0 } = options;
|
||||
|
||||
return await collection
|
||||
.find({ 'moderation.status': status })
|
||||
.sort({ submitted_at: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find high-relevance submissions pending review
|
||||
*/
|
||||
static async findHighRelevance(options = {}) {
|
||||
const collection = await getCollection('case_submissions');
|
||||
const { limit = 10 } = options;
|
||||
|
||||
return await collection
|
||||
.find({
|
||||
'moderation.status': 'pending',
|
||||
'ai_review.relevance_score': { $gte: 0.7 }
|
||||
})
|
||||
.sort({ 'ai_review.relevance_score': -1 })
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update submission
|
||||
*/
|
||||
static async update(id, updates) {
|
||||
const collection = await getCollection('case_submissions');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $set: updates }
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve submission
|
||||
*/
|
||||
static async approve(id, reviewerId, notes = '') {
|
||||
const collection = await getCollection('case_submissions');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
'moderation.status': 'approved',
|
||||
'moderation.reviewer': reviewerId,
|
||||
'moderation.review_notes': notes,
|
||||
'moderation.reviewed_at': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject submission
|
||||
*/
|
||||
static async reject(id, reviewerId, reason) {
|
||||
const collection = await getCollection('case_submissions');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
'moderation.status': 'rejected',
|
||||
'moderation.reviewer': reviewerId,
|
||||
'moderation.review_notes': reason,
|
||||
'moderation.reviewed_at': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request more information
|
||||
*/
|
||||
static async requestInfo(id, reviewerId, requestedInfo) {
|
||||
const collection = await getCollection('case_submissions');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
'moderation.status': 'needs_info',
|
||||
'moderation.reviewer': reviewerId,
|
||||
'moderation.review_notes': requestedInfo,
|
||||
'moderation.reviewed_at': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to published case study
|
||||
*/
|
||||
static async linkPublished(id, publishedCaseId) {
|
||||
const collection = await getCollection('case_submissions');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
published_case_id: new ObjectId(publishedCaseId),
|
||||
'moderation.status': 'approved'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count by status
|
||||
*/
|
||||
static async countByStatus(status) {
|
||||
const collection = await getCollection('case_submissions');
|
||||
return await collection.countDocuments({ 'moderation.status': status });
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete submission
|
||||
*/
|
||||
static async delete(id) {
|
||||
const collection = await getCollection('case_submissions');
|
||||
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
||||
return result.deletedCount > 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CaseSubmission;
|
||||
143
src/models/Document.model.js
Normal file
143
src/models/Document.model.js
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
/**
|
||||
* Document Model
|
||||
* Technical papers, framework documentation, specifications
|
||||
*/
|
||||
|
||||
const { ObjectId } = require('mongodb');
|
||||
const { getCollection } = require('../utils/db.util');
|
||||
|
||||
class Document {
|
||||
/**
|
||||
* Create a new document
|
||||
*/
|
||||
static async create(data) {
|
||||
const collection = await getCollection('documents');
|
||||
|
||||
const document = {
|
||||
title: data.title,
|
||||
slug: data.slug,
|
||||
quadrant: data.quadrant, // STR/OPS/TAC/SYS/STO
|
||||
persistence: data.persistence, // HIGH/MEDIUM/LOW/VARIABLE
|
||||
content_html: data.content_html,
|
||||
content_markdown: data.content_markdown,
|
||||
toc: data.toc || [],
|
||||
metadata: {
|
||||
author: data.metadata?.author || 'John Stroh',
|
||||
date_created: new Date(),
|
||||
date_updated: new Date(),
|
||||
version: data.metadata?.version || '1.0',
|
||||
document_code: data.metadata?.document_code,
|
||||
related_documents: data.metadata?.related_documents || [],
|
||||
tags: data.metadata?.tags || []
|
||||
},
|
||||
translations: data.translations || {},
|
||||
search_index: data.search_index || '',
|
||||
download_formats: data.download_formats || {}
|
||||
};
|
||||
|
||||
const result = await collection.insertOne(document);
|
||||
return { ...document, _id: result.insertedId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find document by ID
|
||||
*/
|
||||
static async findById(id) {
|
||||
const collection = await getCollection('documents');
|
||||
return await collection.findOne({ _id: new ObjectId(id) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find document by slug
|
||||
*/
|
||||
static async findBySlug(slug) {
|
||||
const collection = await getCollection('documents');
|
||||
return await collection.findOne({ slug });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find documents by quadrant
|
||||
*/
|
||||
static async findByQuadrant(quadrant, options = {}) {
|
||||
const collection = await getCollection('documents');
|
||||
const { limit = 50, skip = 0, sort = { 'metadata.date_created': -1 } } = options;
|
||||
|
||||
return await collection
|
||||
.find({ quadrant })
|
||||
.sort(sort)
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search documents
|
||||
*/
|
||||
static async search(query, options = {}) {
|
||||
const collection = await getCollection('documents');
|
||||
const { limit = 20, skip = 0 } = options;
|
||||
|
||||
return await collection
|
||||
.find(
|
||||
{ $text: { $search: query } },
|
||||
{ score: { $meta: 'textScore' } }
|
||||
)
|
||||
.sort({ score: { $meta: 'textScore' } })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update document
|
||||
*/
|
||||
static async update(id, updates) {
|
||||
const collection = await getCollection('documents');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
...updates,
|
||||
'metadata.date_updated': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete document
|
||||
*/
|
||||
static async delete(id) {
|
||||
const collection = await getCollection('documents');
|
||||
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
||||
return result.deletedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all documents
|
||||
*/
|
||||
static async list(options = {}) {
|
||||
const collection = await getCollection('documents');
|
||||
const { limit = 50, skip = 0, sort = { 'metadata.date_created': -1 } } = options;
|
||||
|
||||
return await collection
|
||||
.find({})
|
||||
.sort(sort)
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count documents
|
||||
*/
|
||||
static async count(filter = {}) {
|
||||
const collection = await getCollection('documents');
|
||||
return await collection.countDocuments(filter);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Document;
|
||||
163
src/models/MediaInquiry.model.js
Normal file
163
src/models/MediaInquiry.model.js
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* MediaInquiry Model
|
||||
* Press/media inquiries with AI triage
|
||||
*/
|
||||
|
||||
const { ObjectId } = require('mongodb');
|
||||
const { getCollection } = require('../utils/db.util');
|
||||
|
||||
class MediaInquiry {
|
||||
/**
|
||||
* Create a new media inquiry
|
||||
*/
|
||||
static async create(data) {
|
||||
const collection = await getCollection('media_inquiries');
|
||||
|
||||
const inquiry = {
|
||||
contact: {
|
||||
name: data.contact.name,
|
||||
email: data.contact.email,
|
||||
outlet: data.contact.outlet,
|
||||
phone: data.contact.phone
|
||||
},
|
||||
inquiry: {
|
||||
subject: data.inquiry.subject,
|
||||
message: data.inquiry.message,
|
||||
deadline: data.inquiry.deadline ? new Date(data.inquiry.deadline) : null,
|
||||
topic_areas: data.inquiry.topic_areas || []
|
||||
},
|
||||
ai_triage: {
|
||||
urgency: data.ai_triage?.urgency, // high/medium/low
|
||||
topic_sensitivity: data.ai_triage?.topic_sensitivity,
|
||||
suggested_response_time: data.ai_triage?.suggested_response_time,
|
||||
involves_values: data.ai_triage?.involves_values || false,
|
||||
claude_summary: data.ai_triage?.claude_summary,
|
||||
suggested_talking_points: data.ai_triage?.suggested_talking_points || []
|
||||
},
|
||||
status: data.status || 'new', // new/triaged/responded/closed
|
||||
assigned_to: data.assigned_to,
|
||||
response: {
|
||||
sent_at: data.response?.sent_at,
|
||||
content: data.response?.content,
|
||||
responder: data.response?.responder
|
||||
},
|
||||
created_at: new Date()
|
||||
};
|
||||
|
||||
const result = await collection.insertOne(inquiry);
|
||||
return { ...inquiry, _id: result.insertedId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find inquiry by ID
|
||||
*/
|
||||
static async findById(id) {
|
||||
const collection = await getCollection('media_inquiries');
|
||||
return await collection.findOne({ _id: new ObjectId(id) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find inquiries by status
|
||||
*/
|
||||
static async findByStatus(status, options = {}) {
|
||||
const collection = await getCollection('media_inquiries');
|
||||
const { limit = 20, skip = 0 } = options;
|
||||
|
||||
return await collection
|
||||
.find({ status })
|
||||
.sort({ created_at: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find high urgency inquiries
|
||||
*/
|
||||
static async findUrgent(options = {}) {
|
||||
const collection = await getCollection('media_inquiries');
|
||||
const { limit = 10 } = options;
|
||||
|
||||
return await collection
|
||||
.find({
|
||||
'ai_triage.urgency': 'high',
|
||||
status: { $in: ['new', 'triaged'] }
|
||||
})
|
||||
.sort({ created_at: -1 })
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update inquiry
|
||||
*/
|
||||
static async update(id, updates) {
|
||||
const collection = await getCollection('media_inquiries');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $set: updates }
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign inquiry to user
|
||||
*/
|
||||
static async assign(id, userId) {
|
||||
const collection = await getCollection('media_inquiries');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
assigned_to: new ObjectId(userId),
|
||||
status: 'triaged'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark as responded
|
||||
*/
|
||||
static async respond(id, responseData) {
|
||||
const collection = await getCollection('media_inquiries');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
status: 'responded',
|
||||
'response.sent_at': new Date(),
|
||||
'response.content': responseData.content,
|
||||
'response.responder': responseData.responder
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count by status
|
||||
*/
|
||||
static async countByStatus(status) {
|
||||
const collection = await getCollection('media_inquiries');
|
||||
return await collection.countDocuments({ status });
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete inquiry
|
||||
*/
|
||||
static async delete(id) {
|
||||
const collection = await getCollection('media_inquiries');
|
||||
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
||||
return result.deletedCount > 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MediaInquiry;
|
||||
210
src/models/ModerationQueue.model.js
Normal file
210
src/models/ModerationQueue.model.js
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
/**
|
||||
* ModerationQueue Model
|
||||
* Human oversight queue for AI actions
|
||||
*/
|
||||
|
||||
const { ObjectId } = require('mongodb');
|
||||
const { getCollection } = require('../utils/db.util');
|
||||
|
||||
class ModerationQueue {
|
||||
/**
|
||||
* Add item to moderation queue
|
||||
*/
|
||||
static async create(data) {
|
||||
const collection = await getCollection('moderation_queue');
|
||||
|
||||
const item = {
|
||||
item_type: data.item_type, // blog_post/media_inquiry/case_study/resource
|
||||
item_id: new ObjectId(data.item_id),
|
||||
quadrant: data.quadrant, // STR/OPS/TAC/SYS/STO
|
||||
ai_action: {
|
||||
type: data.ai_action.type, // suggestion/triage/analysis
|
||||
confidence: data.ai_action.confidence, // 0-1
|
||||
reasoning: data.ai_action.reasoning,
|
||||
claude_version: data.ai_action.claude_version || 'claude-sonnet-4-5'
|
||||
},
|
||||
human_required_reason: data.human_required_reason,
|
||||
priority: data.priority || 'medium', // high/medium/low
|
||||
assigned_to: data.assigned_to,
|
||||
status: 'pending', // pending/reviewed/approved/rejected
|
||||
created_at: new Date(),
|
||||
reviewed_at: null,
|
||||
review_decision: {
|
||||
action: null, // approve/reject/modify/escalate
|
||||
notes: null,
|
||||
reviewer: null
|
||||
}
|
||||
};
|
||||
|
||||
const result = await collection.insertOne(item);
|
||||
return { ...item, _id: result.insertedId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find item by ID
|
||||
*/
|
||||
static async findById(id) {
|
||||
const collection = await getCollection('moderation_queue');
|
||||
return await collection.findOne({ _id: new ObjectId(id) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find pending items
|
||||
*/
|
||||
static async findPending(options = {}) {
|
||||
const collection = await getCollection('moderation_queue');
|
||||
const { limit = 20, skip = 0, priority } = options;
|
||||
|
||||
const filter = { status: 'pending' };
|
||||
if (priority) filter.priority = priority;
|
||||
|
||||
return await collection
|
||||
.find(filter)
|
||||
.sort({
|
||||
priority: -1, // high first
|
||||
created_at: 1 // oldest first
|
||||
})
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find by item type
|
||||
*/
|
||||
static async findByType(itemType, options = {}) {
|
||||
const collection = await getCollection('moderation_queue');
|
||||
const { limit = 20, skip = 0 } = options;
|
||||
|
||||
return await collection
|
||||
.find({ item_type: itemType, status: 'pending' })
|
||||
.sort({ created_at: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find by quadrant
|
||||
*/
|
||||
static async findByQuadrant(quadrant, options = {}) {
|
||||
const collection = await getCollection('moderation_queue');
|
||||
const { limit = 20, skip = 0 } = options;
|
||||
|
||||
return await collection
|
||||
.find({ quadrant, status: 'pending' })
|
||||
.sort({ created_at: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Review item (approve/reject/modify/escalate)
|
||||
*/
|
||||
static async review(id, decision) {
|
||||
const collection = await getCollection('moderation_queue');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
status: 'reviewed',
|
||||
reviewed_at: new Date(),
|
||||
'review_decision.action': decision.action,
|
||||
'review_decision.notes': decision.notes,
|
||||
'review_decision.reviewer': decision.reviewer
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve item
|
||||
*/
|
||||
static async approve(id, reviewerId, notes = '') {
|
||||
return await this.review(id, {
|
||||
action: 'approve',
|
||||
notes,
|
||||
reviewer: reviewerId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject item
|
||||
*/
|
||||
static async reject(id, reviewerId, notes) {
|
||||
return await this.review(id, {
|
||||
action: 'reject',
|
||||
notes,
|
||||
reviewer: reviewerId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Escalate to strategic review
|
||||
*/
|
||||
static async escalate(id, reviewerId, reason) {
|
||||
const collection = await getCollection('moderation_queue');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
quadrant: 'STRATEGIC',
|
||||
priority: 'high',
|
||||
human_required_reason: `ESCALATED: ${reason}`,
|
||||
'review_decision.action': 'escalate',
|
||||
'review_decision.notes': reason,
|
||||
'review_decision.reviewer': reviewerId
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count pending items
|
||||
*/
|
||||
static async countPending(filter = {}) {
|
||||
const collection = await getCollection('moderation_queue');
|
||||
return await collection.countDocuments({
|
||||
...filter,
|
||||
status: 'pending'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stats by quadrant
|
||||
*/
|
||||
static async getStatsByQuadrant() {
|
||||
const collection = await getCollection('moderation_queue');
|
||||
|
||||
return await collection.aggregate([
|
||||
{ $match: { status: 'pending' } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$quadrant',
|
||||
count: { $sum: 1 },
|
||||
high_priority: {
|
||||
$sum: { $cond: [{ $eq: ['$priority', 'high'] }, 1, 0] }
|
||||
}
|
||||
}
|
||||
}
|
||||
]).toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete item
|
||||
*/
|
||||
static async delete(id) {
|
||||
const collection = await getCollection('moderation_queue');
|
||||
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
||||
return result.deletedCount > 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ModerationQueue;
|
||||
221
src/models/Resource.model.js
Normal file
221
src/models/Resource.model.js
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
/**
|
||||
* Resource Model
|
||||
* Curated directory of aligned resources
|
||||
*/
|
||||
|
||||
const { ObjectId } = require('mongodb');
|
||||
const { getCollection } = require('../utils/db.util');
|
||||
|
||||
class Resource {
|
||||
/**
|
||||
* Create a new resource
|
||||
*/
|
||||
static async create(data) {
|
||||
const collection = await getCollection('resources');
|
||||
|
||||
const resource = {
|
||||
url: data.url,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
category: data.category, // framework/tool/research/organization/educational
|
||||
subcategory: data.subcategory,
|
||||
alignment_score: data.alignment_score, // 0-1 alignment with Tractatus values
|
||||
ai_analysis: {
|
||||
summary: data.ai_analysis?.summary,
|
||||
relevance: data.ai_analysis?.relevance,
|
||||
quality_indicators: data.ai_analysis?.quality_indicators || [],
|
||||
concerns: data.ai_analysis?.concerns || [],
|
||||
claude_reasoning: data.ai_analysis?.claude_reasoning
|
||||
},
|
||||
status: data.status || 'pending', // pending/approved/rejected
|
||||
reviewed_by: data.reviewed_by,
|
||||
reviewed_at: data.reviewed_at,
|
||||
tags: data.tags || [],
|
||||
featured: data.featured || false,
|
||||
added_at: new Date(),
|
||||
last_checked: new Date()
|
||||
};
|
||||
|
||||
const result = await collection.insertOne(resource);
|
||||
return { ...resource, _id: result.insertedId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find resource by ID
|
||||
*/
|
||||
static async findById(id) {
|
||||
const collection = await getCollection('resources');
|
||||
return await collection.findOne({ _id: new ObjectId(id) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find resource by URL
|
||||
*/
|
||||
static async findByUrl(url) {
|
||||
const collection = await getCollection('resources');
|
||||
return await collection.findOne({ url });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find approved resources
|
||||
*/
|
||||
static async findApproved(options = {}) {
|
||||
const collection = await getCollection('resources');
|
||||
const { limit = 50, skip = 0, category } = options;
|
||||
|
||||
const filter = { status: 'approved' };
|
||||
if (category) filter.category = category;
|
||||
|
||||
return await collection
|
||||
.find(filter)
|
||||
.sort({ alignment_score: -1, added_at: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find featured resources
|
||||
*/
|
||||
static async findFeatured(options = {}) {
|
||||
const collection = await getCollection('resources');
|
||||
const { limit = 10 } = options;
|
||||
|
||||
return await collection
|
||||
.find({ status: 'approved', featured: true })
|
||||
.sort({ alignment_score: -1 })
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find by category
|
||||
*/
|
||||
static async findByCategory(category, options = {}) {
|
||||
const collection = await getCollection('resources');
|
||||
const { limit = 30, skip = 0 } = options;
|
||||
|
||||
return await collection
|
||||
.find({ category, status: 'approved' })
|
||||
.sort({ alignment_score: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find high-alignment pending resources
|
||||
*/
|
||||
static async findHighAlignment(options = {}) {
|
||||
const collection = await getCollection('resources');
|
||||
const { limit = 10 } = options;
|
||||
|
||||
return await collection
|
||||
.find({
|
||||
status: 'pending',
|
||||
alignment_score: { $gte: 0.8 }
|
||||
})
|
||||
.sort({ alignment_score: -1 })
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update resource
|
||||
*/
|
||||
static async update(id, updates) {
|
||||
const collection = await getCollection('resources');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $set: updates }
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve resource
|
||||
*/
|
||||
static async approve(id, reviewerId) {
|
||||
const collection = await getCollection('resources');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
status: 'approved',
|
||||
reviewed_by: reviewerId,
|
||||
reviewed_at: new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject resource
|
||||
*/
|
||||
static async reject(id, reviewerId) {
|
||||
const collection = await getCollection('resources');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
status: 'rejected',
|
||||
reviewed_by: reviewerId,
|
||||
reviewed_at: new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark as featured
|
||||
*/
|
||||
static async setFeatured(id, featured = true) {
|
||||
const collection = await getCollection('resources');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $set: { featured } }
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last checked timestamp
|
||||
*/
|
||||
static async updateLastChecked(id) {
|
||||
const collection = await getCollection('resources');
|
||||
|
||||
await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $set: { last_checked: new Date() } }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count by status
|
||||
*/
|
||||
static async countByStatus(status) {
|
||||
const collection = await getCollection('resources');
|
||||
return await collection.countDocuments({ status });
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete resource
|
||||
*/
|
||||
static async delete(id) {
|
||||
const collection = await getCollection('resources');
|
||||
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
||||
return result.deletedCount > 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Resource;
|
||||
177
src/models/User.model.js
Normal file
177
src/models/User.model.js
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
/**
|
||||
* User Model
|
||||
* Admin user accounts
|
||||
*/
|
||||
|
||||
const { ObjectId } = require('mongodb');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { getCollection } = require('../utils/db.util');
|
||||
|
||||
class User {
|
||||
/**
|
||||
* Create a new user
|
||||
*/
|
||||
static async create(data) {
|
||||
const collection = await getCollection('users');
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||
|
||||
const user = {
|
||||
email: data.email,
|
||||
password: hashedPassword,
|
||||
name: data.name,
|
||||
role: data.role || 'admin', // admin/moderator/viewer
|
||||
created_at: new Date(),
|
||||
last_login: null,
|
||||
active: data.active !== undefined ? data.active : true
|
||||
};
|
||||
|
||||
const result = await collection.insertOne(user);
|
||||
|
||||
// Return user without password
|
||||
const { password, ...userWithoutPassword } = { ...user, _id: result.insertedId };
|
||||
return userWithoutPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user by ID
|
||||
*/
|
||||
static async findById(id) {
|
||||
const collection = await getCollection('users');
|
||||
const user = await collection.findOne({ _id: new ObjectId(id) });
|
||||
|
||||
if (user) {
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user by email
|
||||
*/
|
||||
static async findByEmail(email) {
|
||||
const collection = await getCollection('users');
|
||||
return await collection.findOne({ email: email.toLowerCase() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate user
|
||||
*/
|
||||
static async authenticate(email, password) {
|
||||
const user = await this.findByEmail(email);
|
||||
|
||||
if (!user || !user.active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update last login
|
||||
await this.updateLastLogin(user._id);
|
||||
|
||||
// Return user without password
|
||||
const { password: _, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last login timestamp
|
||||
*/
|
||||
static async updateLastLogin(id) {
|
||||
const collection = await getCollection('users');
|
||||
|
||||
await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $set: { last_login: new Date() } }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user
|
||||
*/
|
||||
static async update(id, updates) {
|
||||
const collection = await getCollection('users');
|
||||
|
||||
// Remove password from updates (use changePassword for that)
|
||||
const { password, ...safeUpdates } = updates;
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $set: safeUpdates }
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change password
|
||||
*/
|
||||
static async changePassword(id, newPassword) {
|
||||
const collection = await getCollection('users');
|
||||
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $set: { password: hashedPassword } }
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate user
|
||||
*/
|
||||
static async deactivate(id) {
|
||||
const collection = await getCollection('users');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $set: { active: false } }
|
||||
);
|
||||
|
||||
return result.modifiedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all users
|
||||
*/
|
||||
static async list(options = {}) {
|
||||
const collection = await getCollection('users');
|
||||
const { limit = 50, skip = 0 } = options;
|
||||
|
||||
const users = await collection
|
||||
.find({}, { projection: { password: 0 } })
|
||||
.sort({ created_at: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count users
|
||||
*/
|
||||
static async count(filter = {}) {
|
||||
const collection = await getCollection('users');
|
||||
return await collection.countDocuments(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user
|
||||
*/
|
||||
static async delete(id) {
|
||||
const collection = await getCollection('users');
|
||||
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
||||
return result.deletedCount > 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = User;
|
||||
22
src/models/index.js
Normal file
22
src/models/index.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Models Index
|
||||
* Export all models
|
||||
*/
|
||||
|
||||
const Document = require('./Document.model');
|
||||
const BlogPost = require('./BlogPost.model');
|
||||
const MediaInquiry = require('./MediaInquiry.model');
|
||||
const CaseSubmission = require('./CaseSubmission.model');
|
||||
const Resource = require('./Resource.model');
|
||||
const ModerationQueue = require('./ModerationQueue.model');
|
||||
const User = require('./User.model');
|
||||
|
||||
module.exports = {
|
||||
Document,
|
||||
BlogPost,
|
||||
MediaInquiry,
|
||||
CaseSubmission,
|
||||
Resource,
|
||||
ModerationQueue,
|
||||
User
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue