Files
metabuilder/packages/forum_forge/seed/scripts/db/operations.lua
2025-12-30 22:21:12 +00:00

370 lines
10 KiB
Lua

-- forum_forge/seed/scripts/db/operations.lua
-- DBAL operations for Forum entities (Category, Thread, Post)
-- @module forum_forge.db.operations
local M = {}
local json = require('json')
local prefix = require('shared.db.prefix')
-- Package ID for entity prefixing
local PACKAGE_ID = 'forum_forge'
-- Helper to get prefixed entity name
local function entity(name)
return prefix.getPrefixedName(PACKAGE_ID, name)
end
---------------------------------------------------------------------------
-- CATEGORY OPERATIONS
---------------------------------------------------------------------------
---@class ForumCategoryCreateParams
---@field tenantId string
---@field name string
---@field slug string
---@field description string|nil
---@field icon string|nil
---@field color string|nil
---@field parentId string|nil
---@field minLevel number|nil
---Create a new forum category
---@param dbal table DBAL client instance
---@param params ForumCategoryCreateParams
---@return table Created category
function M.createCategory(dbal, params)
-- Get next sort order
local existing = dbal:list(entity('ForumCategory'), {
where = { tenantId = params.tenantId },
orderBy = { sortOrder = 'desc' },
take = 1,
})
local maxOrder = 0
if existing.items and #existing.items > 0 then
maxOrder = existing.items[1].sortOrder or 0
end
return dbal:create(entity('ForumCategory'), {
tenantId = params.tenantId,
name = params.name,
slug = params.slug,
description = params.description,
icon = params.icon,
color = params.color,
parentId = params.parentId,
sortOrder = maxOrder + 1,
minLevel = params.minLevel or 0,
threadCount = 0,
postCount = 0,
createdAt = os.time() * 1000,
updatedAt = os.time() * 1000,
})
end
---List categories
---@param dbal table DBAL client instance
---@param tenantId string
---@param parentId string|nil Filter by parent (nil for root)
---@return table[] Categories
function M.listCategories(dbal, tenantId, parentId)
local where = { tenantId = tenantId }
if parentId then
where.parentId = parentId
end
local result = dbal:list(entity('ForumCategory'), {
where = where,
orderBy = { sortOrder = 'asc' },
take = 100,
})
return result.items or {}
end
---Update category
---@param dbal table
---@param categoryId string
---@param updates table
---@return table Updated category
function M.updateCategory(dbal, categoryId, updates)
updates.updatedAt = os.time() * 1000
return dbal:update(entity('ForumCategory'), categoryId, updates)
end
end
---------------------------------------------------------------------------
-- THREAD OPERATIONS
---------------------------------------------------------------------------
---@class ForumThreadCreateParams
---@field tenantId string
---@field categoryId string
---@field authorId string
---@field authorName string
---@field title string
---@field tags table|nil Array of tag strings
---@field isPinned boolean|nil
---@field isLocked boolean|nil
---Create a new thread
---@param dbal table DBAL client instance
---@param params ForumThreadCreateParams
---@return table Created thread
function M.createThread(dbal, params)
local thread = dbal:create(entity('ForumThread'), {
tenantId = params.tenantId,
categoryId = params.categoryId,
authorId = params.authorId,
authorName = params.authorName,
title = params.title,
slug = M._slugify(params.title),
tags = params.tags and json.encode(params.tags) or '[]',
isPinned = params.isPinned or false,
isLocked = params.isLocked or false,
viewCount = 0,
replyCount = 0,
createdAt = os.time() * 1000,
updatedAt = os.time() * 1000,
lastPostAt = os.time() * 1000,
lastPostAuthorId = params.authorId,
lastPostAuthorName = params.authorName,
})
-- Increment category thread count
local category = dbal:read(entity('ForumCategory'), params.categoryId)
if category then
dbal:update(entity('ForumCategory'), params.categoryId, {
threadCount = (category.threadCount or 0) + 1,
updatedAt = os.time() * 1000,
})
end
return thread
end
---List threads in a category
---@param dbal table DBAL client instance
---@param categoryId string
---@param take number|nil
---@param skip number|nil
---@return table List result
function M.listThreads(dbal, categoryId, take, skip)
return dbal:list(entity('ForumThread'), {
where = { categoryId = categoryId },
orderBy = { isPinned = 'desc', lastPostAt = 'desc' },
take = take or 20,
skip = skip or 0,
})
end
---Get thread by ID
---@param dbal table DBAL client instance
---@param threadId string
---@param incrementView boolean|nil Increment view count
---@return table|nil Thread
function M.getThread(dbal, threadId, incrementView)
local thread = dbal:read(entity('ForumThread'), threadId)
if thread and incrementView then
dbal:update(entity('ForumThread'), threadId, {
viewCount = (thread.viewCount or 0) + 1,
})
thread.viewCount = (thread.viewCount or 0) + 1
end
return thread
end
---Update thread
---@param dbal table
---@param threadId string
---@param updates table
---@return table Updated thread
function M.updateThread(dbal, threadId, updates)
updates.updatedAt = os.time() * 1000
return dbal:update(entity('ForumThread'), threadId, updates)
end
end
---Pin/unpin a thread
---@param dbal table
---@param threadId string
---@param isPinned boolean
function M.pinThread(dbal, threadId, isPinned)
return M.updateThread(dbal, threadId, { isPinned = isPinned })
end
---Lock/unlock a thread
---@param dbal table
---@param threadId string
---@param isLocked boolean
function M.lockThread(dbal, threadId, isLocked)
return M.updateThread(dbal, threadId, { isLocked = isLocked })
end
---------------------------------------------------------------------------
-- POST OPERATIONS
---------------------------------------------------------------------------
---@class ForumPostCreateParams
---@field tenantId string
---@field threadId string
---@field authorId string
---@field authorName string
---@field content string
---@field replyToId string|nil
---Create a new post (reply)
---@param dbal table DBAL client instance
---@param params ForumPostCreateParams
---@return table Created post
function M.createPost(dbal, params)
local thread = dbal:read(entity('ForumThread'), params.threadId)
if not thread then
error('Thread not found: ' .. params.threadId)
end
if thread.isLocked then
error('Thread is locked')
end
local post = dbal:create(entity('ForumPost'), {
tenantId = params.tenantId,
threadId = params.threadId,
authorId = params.authorId,
authorName = params.authorName,
content = params.content,
replyToId = params.replyToId,
isEdited = false,
likeCount = 0,
createdAt = os.time() * 1000,
updatedAt = os.time() * 1000,
})
-- Update thread stats
dbal:update(entity('ForumThread'), params.threadId, {
replyCount = (thread.replyCount or 0) + 1,
lastPostAt = os.time() * 1000,
lastPostAuthorId = params.authorId,
lastPostAuthorName = params.authorName,
updatedAt = os.time() * 1000,
})
-- Update category post count
local category = dbal:read(entity('ForumCategory'), thread.categoryId)
if category then
dbal:update(entity('ForumCategory'), thread.categoryId, {
postCount = (category.postCount or 0) + 1,
updatedAt = os.time() * 1000,
})
end
return post
end
---List posts in a thread
---@param dbal table DBAL client instance
---@param threadId string
---@param take number|nil
---@param skip number|nil
---@return table List result
function M.listPosts(dbal, threadId, take, skip)
return dbal:list(entity('ForumPost'), {
where = { threadId = threadId },
orderBy = { createdAt = 'asc' },
take = take or 50,
skip = skip or 0,
})
end
---Update post content
---@param dbal table
---@param postId string
---@param content string
---@return table Updated post
function M.updatePost(dbal, postId, content)
return dbal:update(entity('ForumPost'), postId, {
content = content,
isEdited = true,
editedAt = os.time() * 1000,
updatedAt = os.time() * 1000,
})
end
---Like a post
---@param dbal table
---@param postId string
---@return table Updated post
function M.likePost(dbal, postId)
local post = dbal:read(entity('ForumPost'), postId)
if post then
return dbal:update(entity('ForumPost'), postId, {
likeCount = (post.likeCount or 0) + 1,
})
end
return post
end
---Delete a post (soft delete by clearing content)
---@param dbal table
---@param postId string
---@param deletedBy string Username who deleted
---@return table Updated post
function M.deletePost(dbal, postId, deletedBy)
return dbal:update(entity('ForumPost'), postId, {
content = '[Deleted by ' .. deletedBy .. ']',
isEdited = true,
editedAt = os.time() * 1000,
updatedAt = os.time() * 1000,
})
end
end
---------------------------------------------------------------------------
-- UTILITY FUNCTIONS
---------------------------------------------------------------------------
---Generate a URL-safe slug from text
---@param text string
---@return string Slug
function M._slugify(text)
local slug = text:lower()
slug = slug:gsub('[^%w%s-]', '')
slug = slug:gsub('%s+', '-')
slug = slug:gsub('-+', '-')
slug = slug:gsub('^-', ''):gsub('-$', '')
return slug:sub(1, 100)
end
---Search threads by title
---@param dbal table
---@param tenantId string
---@param query string
---@param take number|nil
---@return table[] Matching threads
function M.searchThreads(dbal, tenantId, query, take)
-- Basic implementation - fetch and filter
-- TODO: Implement proper full-text search via DBAL
local result = dbal:list(entity('ForumThread'), {
where = { tenantId = tenantId },
take = 1000,
})
local matches = {}
local lowerQuery = query:lower()
for _, thread in ipairs(result.items or {}) do
if thread.title:lower():find(lowerQuery, 1, true) then
table.insert(matches, thread)
if #matches >= (take or 20) then
break
end
end
end
return matches
end
return M