diff --git a/packages/github_tools/seed/scripts/db/operations.lua b/packages/github_tools/seed/scripts/db/operations.lua new file mode 100644 index 000000000..30f57260b --- /dev/null +++ b/packages/github_tools/seed/scripts/db/operations.lua @@ -0,0 +1,317 @@ +-- github_tools/seed/scripts/db/operations.lua +-- DBAL operations for GitHub integration settings +-- @module github_tools.db.operations + +local M = {} +local json = require('json') + +--------------------------------------------------------------------------- +-- GITHUB CONNECTION OPERATIONS +--------------------------------------------------------------------------- + +---@class GitHubConnectionParams +---@field tenantId string +---@field userId string +---@field accessToken string Encrypted token +---@field username string +---@field avatarUrl string|nil +---@field scopes table|nil + +---Save GitHub connection +---@param dbal table DBAL client instance +---@param params GitHubConnectionParams +---@return table Created/updated connection +function M.saveConnection(dbal, params) + -- Check for existing connection + local existing = dbal:findFirst('GitHubConnection', { + where = { tenantId = params.tenantId, userId = params.userId }, + }) + + if existing then + return dbal:update('GitHubConnection', existing.id, { + accessToken = params.accessToken, + username = params.username, + avatarUrl = params.avatarUrl, + scopes = params.scopes and json.encode(params.scopes) or nil, + updatedAt = os.time() * 1000, + }) + end + + return dbal:create('GitHubConnection', { + tenantId = params.tenantId, + userId = params.userId, + accessToken = params.accessToken, + username = params.username, + avatarUrl = params.avatarUrl, + scopes = params.scopes and json.encode(params.scopes) or nil, + createdAt = os.time() * 1000, + updatedAt = os.time() * 1000, + }) +end + +---Get user's GitHub connection +---@param dbal table +---@param tenantId string +---@param userId string +---@return table|nil Connection +function M.getConnection(dbal, tenantId, userId) + return dbal:findFirst('GitHubConnection', { + where = { tenantId = tenantId, userId = userId }, + }) +end + +---Delete GitHub connection +---@param dbal table +---@param tenantId string +---@param userId string +---@return boolean Success +function M.deleteConnection(dbal, tenantId, userId) + local connection = M.getConnection(dbal, tenantId, userId) + if connection then + return dbal:delete('GitHubConnection', connection.id) + end + return false +end + +---Check if user has GitHub connected +---@param dbal table +---@param tenantId string +---@param userId string +---@return boolean +function M.isConnected(dbal, tenantId, userId) + return M.getConnection(dbal, tenantId, userId) ~= nil +end + +--------------------------------------------------------------------------- +-- REPOSITORY OPERATIONS +--------------------------------------------------------------------------- + +---@class GitHubRepoParams +---@field tenantId string +---@field connectionId string +---@field owner string +---@field name string +---@field fullName string +---@field description string|nil +---@field isPrivate boolean +---@field defaultBranch string + +---Sync a repository +---@param dbal table +---@param params GitHubRepoParams +---@return table Created/updated repo +function M.syncRepository(dbal, params) + local existing = dbal:findFirst('GitHubRepository', { + where = { tenantId = params.tenantId, fullName = params.fullName }, + }) + + if existing then + return dbal:update('GitHubRepository', existing.id, { + description = params.description, + isPrivate = params.isPrivate, + defaultBranch = params.defaultBranch, + syncedAt = os.time() * 1000, + }) + end + + return dbal:create('GitHubRepository', { + tenantId = params.tenantId, + connectionId = params.connectionId, + owner = params.owner, + name = params.name, + fullName = params.fullName, + description = params.description, + isPrivate = params.isPrivate, + defaultBranch = params.defaultBranch, + createdAt = os.time() * 1000, + syncedAt = os.time() * 1000, + }) +end + +---List synced repositories +---@param dbal table +---@param connectionId string +---@return table[] Repositories +function M.listRepositories(dbal, connectionId) + local result = dbal:list('GitHubRepository', { + where = { connectionId = connectionId }, + orderBy = { name = 'asc' }, + take = 200, + }) + return result.items or {} +end + +---Get repository by full name +---@param dbal table +---@param tenantId string +---@param fullName string e.g., "owner/repo" +---@return table|nil Repository +function M.getRepository(dbal, tenantId, fullName) + return dbal:findFirst('GitHubRepository', { + where = { tenantId = tenantId, fullName = fullName }, + }) +end + +---Delete repository record +---@param dbal table +---@param repoId string +---@return boolean +function M.deleteRepository(dbal, repoId) + return dbal:delete('GitHubRepository', repoId) +end + +--------------------------------------------------------------------------- +-- WEBHOOK OPERATIONS +--------------------------------------------------------------------------- + +---@class WebhookCreateParams +---@field tenantId string +---@field repositoryId string +---@field webhookId string GitHub's webhook ID +---@field events table[] Events to listen for +---@field secret string Webhook secret + +---Register a webhook +---@param dbal table +---@param params WebhookCreateParams +---@return table Created webhook +function M.registerWebhook(dbal, params) + return dbal:create('GitHubWebhook', { + tenantId = params.tenantId, + repositoryId = params.repositoryId, + webhookId = params.webhookId, + events = json.encode(params.events or {}), + secret = params.secret, + isActive = true, + createdAt = os.time() * 1000, + }) +end + +---Get webhook for repository +---@param dbal table +---@param repositoryId string +---@return table|nil Webhook +function M.getWebhook(dbal, repositoryId) + return dbal:findFirst('GitHubWebhook', { + where = { repositoryId = repositoryId, isActive = true }, + }) +end + +---Deactivate webhook +---@param dbal table +---@param webhookId string +function M.deactivateWebhook(dbal, webhookId) + return dbal:update('GitHubWebhook', webhookId, { + isActive = false, + updatedAt = os.time() * 1000, + }) +end + +--------------------------------------------------------------------------- +-- WEBHOOK EVENT LOG +--------------------------------------------------------------------------- + +---Log a webhook event +---@param dbal table +---@param tenantId string +---@param repositoryId string +---@param event string push|pull_request|issue|etc +---@param payload table Event payload +---@param processed boolean +function M.logWebhookEvent(dbal, tenantId, repositoryId, event, payload, processed) + return dbal:create('GitHubWebhookEvent', { + tenantId = tenantId, + repositoryId = repositoryId, + event = event, + payload = json.encode(payload), + processed = processed, + receivedAt = os.time() * 1000, + }) +end + +---List recent webhook events +---@param dbal table +---@param repositoryId string +---@param take number|nil +---@return table[] Events +function M.listWebhookEvents(dbal, repositoryId, take) + local result = dbal:list('GitHubWebhookEvent', { + where = { repositoryId = repositoryId }, + orderBy = { receivedAt = 'desc' }, + take = take or 50, + }) + return result.items or {} +end + +---Mark event as processed +---@param dbal table +---@param eventId string +function M.markEventProcessed(dbal, eventId) + return dbal:update('GitHubWebhookEvent', eventId, { + processed = true, + processedAt = os.time() * 1000, + }) +end + +--------------------------------------------------------------------------- +-- PULL REQUEST TRACKING +--------------------------------------------------------------------------- + +---Track a pull request +---@param dbal table +---@param tenantId string +---@param repositoryId string +---@param prNumber number +---@param title string +---@param author string +---@param status string open|closed|merged +---@param url string +function M.trackPullRequest(dbal, tenantId, repositoryId, prNumber, title, author, status, url) + local existing = dbal:findFirst('GitHubPullRequest', { + where = { repositoryId = repositoryId, prNumber = prNumber }, + }) + + if existing then + return dbal:update('GitHubPullRequest', existing.id, { + title = title, + status = status, + updatedAt = os.time() * 1000, + }) + end + + return dbal:create('GitHubPullRequest', { + tenantId = tenantId, + repositoryId = repositoryId, + prNumber = prNumber, + title = title, + author = author, + status = status, + url = url, + createdAt = os.time() * 1000, + updatedAt = os.time() * 1000, + }) +end + +---List pull requests for repository +---@param dbal table +---@param repositoryId string +---@param status string|nil Filter by status +---@param take number|nil +---@return table[] Pull requests +function M.listPullRequests(dbal, repositoryId, status, take) + local where = { repositoryId = repositoryId } + + if status then + where.status = status + end + + local result = dbal:list('GitHubPullRequest', { + where = where, + orderBy = { updatedAt = 'desc' }, + take = take or 20, + }) + + return result.items or {} +end + +return M diff --git a/packages/smtp_config/seed/scripts/db/operations.lua b/packages/smtp_config/seed/scripts/db/operations.lua new file mode 100644 index 000000000..c8d2d64f8 --- /dev/null +++ b/packages/smtp_config/seed/scripts/db/operations.lua @@ -0,0 +1,372 @@ +-- smtp_config/seed/scripts/db/operations.lua +-- DBAL operations for SMTP configuration +-- @module smtp_config.db.operations + +local M = {} +local json = require('json') + +--------------------------------------------------------------------------- +-- SMTP CONFIGURATION OPERATIONS +--------------------------------------------------------------------------- + +---@class SMTPConfigParams +---@field tenantId string +---@field name string Configuration name +---@field host string SMTP host +---@field port number SMTP port +---@field secure boolean Use TLS +---@field username string +---@field password string Encrypted password +---@field fromName string Default sender name +---@field fromEmail string Default sender email +---@field replyTo string|nil + +---Create or update SMTP configuration +---@param dbal table DBAL client instance +---@param params SMTPConfigParams +---@return table Created/updated config +function M.saveConfig(dbal, params) + -- Check for existing config with same name + local existing = dbal:findFirst('SMTPConfig', { + where = { tenantId = params.tenantId, name = params.name }, + }) + + if existing then + return dbal:update('SMTPConfig', existing.id, { + host = params.host, + port = params.port, + secure = params.secure, + username = params.username, + password = params.password, + fromName = params.fromName, + fromEmail = params.fromEmail, + replyTo = params.replyTo, + updatedAt = os.time() * 1000, + }) + end + + return dbal:create('SMTPConfig', { + tenantId = params.tenantId, + name = params.name, + host = params.host, + port = params.port, + secure = params.secure, + username = params.username, + password = params.password, + fromName = params.fromName, + fromEmail = params.fromEmail, + replyTo = params.replyTo, + isDefault = false, + isVerified = false, + createdAt = os.time() * 1000, + updatedAt = os.time() * 1000, + }) +end + +---Get configuration by ID +---@param dbal table +---@param configId string +---@return table|nil Config +function M.getConfig(dbal, configId) + return dbal:read('SMTPConfig', configId) +end + +---Get default configuration for tenant +---@param dbal table +---@param tenantId string +---@return table|nil Default config +function M.getDefaultConfig(dbal, tenantId) + return dbal:findFirst('SMTPConfig', { + where = { tenantId = tenantId, isDefault = true }, + }) +end + +---List all configurations for tenant +---@param dbal table +---@param tenantId string +---@return table[] Configs +function M.listConfigs(dbal, tenantId) + local result = dbal:list('SMTPConfig', { + where = { tenantId = tenantId }, + orderBy = { name = 'asc' }, + take = 50, + }) + return result.items or {} +end + +---Set as default configuration +---@param dbal table +---@param tenantId string +---@param configId string +function M.setDefault(dbal, tenantId, configId) + -- Clear existing default + local configs = M.listConfigs(dbal, tenantId) + for _, config in ipairs(configs) do + if config.isDefault then + dbal:update('SMTPConfig', config.id, { isDefault = false }) + end + end + + -- Set new default + return dbal:update('SMTPConfig', configId, { + isDefault = true, + updatedAt = os.time() * 1000, + }) +end + +---Mark configuration as verified +---@param dbal table +---@param configId string +function M.markVerified(dbal, configId) + return dbal:update('SMTPConfig', configId, { + isVerified = true, + verifiedAt = os.time() * 1000, + updatedAt = os.time() * 1000, + }) +end + +---Mark configuration as failed +---@param dbal table +---@param configId string +---@param error string +function M.markFailed(dbal, configId, error) + return dbal:update('SMTPConfig', configId, { + isVerified = false, + lastError = error, + updatedAt = os.time() * 1000, + }) +end + +---Delete configuration +---@param dbal table +---@param configId string +---@return boolean Success +function M.deleteConfig(dbal, configId) + return dbal:delete('SMTPConfig', configId) +end + +--------------------------------------------------------------------------- +-- EMAIL TEMPLATE OPERATIONS +--------------------------------------------------------------------------- + +---@class EmailTemplateParams +---@field tenantId string +---@field name string Template name/key +---@field subject string Email subject (can include placeholders) +---@field bodyHtml string HTML body +---@field bodyText string|nil Plain text body +---@field variables table|nil Variable definitions + +---Create email template +---@param dbal table +---@param params EmailTemplateParams +---@return table Created template +function M.createTemplate(dbal, params) + return dbal:create('EmailTemplate', { + tenantId = params.tenantId, + name = params.name, + subject = params.subject, + bodyHtml = params.bodyHtml, + bodyText = params.bodyText, + variables = params.variables and json.encode(params.variables) or '[]', + isActive = true, + createdAt = os.time() * 1000, + updatedAt = os.time() * 1000, + }) +end + +---Get template by name +---@param dbal table +---@param tenantId string +---@param name string +---@return table|nil Template +function M.getTemplate(dbal, tenantId, name) + local template = dbal:findFirst('EmailTemplate', { + where = { tenantId = tenantId, name = name, isActive = true }, + }) + if template and template.variables then + template.variables = json.decode(template.variables) + end + return template +end + +---List all templates +---@param dbal table +---@param tenantId string +---@return table[] Templates +function M.listTemplates(dbal, tenantId) + local result = dbal:list('EmailTemplate', { + where = { tenantId = tenantId }, + orderBy = { name = 'asc' }, + take = 100, + }) + return result.items or {} +end + +---Update template +---@param dbal table +---@param templateId string +---@param updates table +---@return table Updated template +function M.updateTemplate(dbal, templateId, updates) + if updates.variables and type(updates.variables) == 'table' then + updates.variables = json.encode(updates.variables) + end + updates.updatedAt = os.time() * 1000 + return dbal:update('EmailTemplate', templateId, updates) +end + +---Delete template +---@param dbal table +---@param templateId string +---@return boolean Success +function M.deleteTemplate(dbal, templateId) + return dbal:delete('EmailTemplate', templateId) +end + +--------------------------------------------------------------------------- +-- EMAIL LOG OPERATIONS +--------------------------------------------------------------------------- + +---Log an email send +---@param dbal table +---@param tenantId string +---@param configId string +---@param to string +---@param subject string +---@param templateName string|nil +---@param status string sent|failed|queued +---@param error string|nil +function M.logEmail(dbal, tenantId, configId, to, subject, templateName, status, error) + return dbal:create('EmailLog', { + tenantId = tenantId, + configId = configId, + recipient = to, + subject = subject, + templateName = templateName, + status = status, + error = error, + sentAt = os.time() * 1000, + }) +end + +---List email logs +---@param dbal table +---@param tenantId string +---@param status string|nil Filter by status +---@param take number|nil +---@return table[] Logs +function M.listEmailLogs(dbal, tenantId, status, take) + local where = { tenantId = tenantId } + + if status then + where.status = status + end + + local result = dbal:list('EmailLog', { + where = where, + orderBy = { sentAt = 'desc' }, + take = take or 50, + }) + + return result.items or {} +end + +---Get email stats +---@param dbal table +---@param tenantId string +---@param days number|nil Number of days to look back +---@return table Stats +function M.getEmailStats(dbal, tenantId, days) + local cutoff = (os.time() - (days or 30) * 86400) * 1000 + + local result = dbal:list('EmailLog', { + where = { tenantId = tenantId }, + take = 10000, + }) + + local stats = { + total = 0, + sent = 0, + failed = 0, + queued = 0, + } + + for _, log in ipairs(result.items or {}) do + if log.sentAt >= cutoff then + stats.total = stats.total + 1 + local status = log.status or 'sent' + stats[status] = (stats[status] or 0) + 1 + end + end + + return stats +end + +--------------------------------------------------------------------------- +-- DEFAULT TEMPLATES +--------------------------------------------------------------------------- + +---Create default email templates +---@param dbal table +---@param tenantId string +---@return table[] Created templates +function M.createDefaultTemplates(dbal, tenantId) + local templates = {} + + -- Welcome email + table.insert(templates, M.createTemplate(dbal, { + tenantId = tenantId, + name = 'welcome', + subject = 'Welcome to {{appName}}!', + bodyHtml = [[ +

Welcome, {{username}}!

+

Thank you for joining {{appName}}.

+

Click here to get started.

+ ]], + bodyText = 'Welcome, {{username}}! Thank you for joining {{appName}}.', + variables = { + { name = 'username', required = true }, + { name = 'appName', required = true }, + { name = 'loginUrl', required = true }, + }, + })) + + -- Password reset + table.insert(templates, M.createTemplate(dbal, { + tenantId = tenantId, + name = 'password-reset', + subject = 'Reset Your Password', + bodyHtml = [[ +

Password Reset

+

Click the link below to reset your password:

+

Reset Password

+

This link expires in {{expiresIn}} hours.

+ ]], + bodyText = 'Reset your password: {{resetUrl}}', + variables = { + { name = 'resetUrl', required = true }, + { name = 'expiresIn', default = '24' }, + }, + })) + + -- Email verification + table.insert(templates, M.createTemplate(dbal, { + tenantId = tenantId, + name = 'verify-email', + subject = 'Verify Your Email', + bodyHtml = [[ +

Verify Your Email

+

Click the link below to verify your email address:

+

Verify Email

+ ]], + bodyText = 'Verify your email: {{verifyUrl}}', + variables = { + { name = 'verifyUrl', required = true }, + }, + })) + + return templates +end + +return M