feat: add tenant and user management with migrations, handlers, and tests
- Implemented tenant and user management features including: - Tenant creation and management with associated migrations. - User creation and management with associated migrations. - Tenant membership management with associated migrations. - Platform user roles management with associated migrations. - Quota management with associated migrations. - Article and template management with associated migrations. - Added HTTP handlers for templates and workspaces. - Created tests for protected and public routes. - Introduced a script to check tenant scope in SQL queries. - Documented task plan for backend completion and frontend foundation.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var promptVariablePattern = regexp.MustCompile(`\{\{\s*([a-zA-Z0-9_]+)\s*\}\}`)
|
||||
|
||||
func buildGenerationPrompt(templateName string, promptTemplate *string, params map[string]interface{}) string {
|
||||
basePrompt := strings.TrimSpace(renderPromptTemplate(promptTemplate, params))
|
||||
if basePrompt == "" {
|
||||
basePrompt = fmt.Sprintf("Write a complete article in markdown for the template %q.", templateName)
|
||||
}
|
||||
|
||||
var sections []string
|
||||
sections = append(sections, basePrompt)
|
||||
|
||||
contextBlock := buildPromptContext(params)
|
||||
if contextBlock != "" {
|
||||
sections = append(sections, "Request context:\n"+contextBlock)
|
||||
}
|
||||
|
||||
sections = append(sections, strings.Join([]string{
|
||||
"Output requirements:",
|
||||
"- Return only article markdown, no surrounding explanation.",
|
||||
"- Use the provided title when available.",
|
||||
"- Follow the provided outline sections and key points when available.",
|
||||
"- Keep structure clear with headings, short paragraphs, and concrete details.",
|
||||
}, "\n"))
|
||||
|
||||
return strings.Join(sections, "\n\n")
|
||||
}
|
||||
|
||||
func renderPromptTemplate(promptTemplate *string, params map[string]interface{}) string {
|
||||
if promptTemplate == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return promptVariablePattern.ReplaceAllStringFunc(*promptTemplate, func(match string) string {
|
||||
submatches := promptVariablePattern.FindStringSubmatch(match)
|
||||
if len(submatches) != 2 {
|
||||
return match
|
||||
}
|
||||
|
||||
value, ok := params[submatches[1]]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return formatPromptValue(value)
|
||||
})
|
||||
}
|
||||
|
||||
func buildPromptContext(params map[string]interface{}) string {
|
||||
if len(params) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
orderedKeys := []string{
|
||||
"locale",
|
||||
"title",
|
||||
"topic",
|
||||
"product_name",
|
||||
"subject",
|
||||
"brand_name",
|
||||
"brand",
|
||||
"category",
|
||||
"count",
|
||||
"depth",
|
||||
"outline_sections",
|
||||
"key_points",
|
||||
"keywords",
|
||||
"competitors",
|
||||
}
|
||||
|
||||
used := make(map[string]struct{}, len(params))
|
||||
lines := make([]string, 0, len(params))
|
||||
|
||||
appendLine := func(key string, value interface{}) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
formatted := strings.TrimSpace(formatPromptValue(value))
|
||||
if formatted == "" {
|
||||
return
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("- %s: %s", key, formatted))
|
||||
used[key] = struct{}{}
|
||||
}
|
||||
|
||||
for _, key := range orderedKeys {
|
||||
appendLine(key, params[key])
|
||||
}
|
||||
|
||||
for key, value := range params {
|
||||
if _, ok := used[key]; ok {
|
||||
continue
|
||||
}
|
||||
appendLine(key, value)
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func formatPromptValue(value interface{}) string {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return strings.TrimSpace(v)
|
||||
case []string:
|
||||
return strings.Join(v, ", ")
|
||||
case []interface{}:
|
||||
parts := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
formatted := strings.TrimSpace(formatPromptValue(item))
|
||||
if formatted != "" {
|
||||
parts = append(parts, formatted)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
default:
|
||||
bytes, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveArticleTitle(params map[string]interface{}, markdown string) string {
|
||||
if title := strings.TrimSpace(stringValue(params["title"])); title != "" {
|
||||
return title
|
||||
}
|
||||
if topic := strings.TrimSpace(stringValue(params["topic"])); topic != "" {
|
||||
return topic
|
||||
}
|
||||
if product := strings.TrimSpace(stringValue(params["product_name"])); product != "" {
|
||||
return product + " Review"
|
||||
}
|
||||
if subject := strings.TrimSpace(stringValue(params["subject"])); subject != "" {
|
||||
return subject + " Report"
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(markdown, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "#") {
|
||||
return strings.TrimSpace(strings.TrimLeft(line, "#"))
|
||||
}
|
||||
}
|
||||
return "Generated Article"
|
||||
}
|
||||
|
||||
func estimateWordCount(markdown string) int {
|
||||
trimmed := strings.TrimSpace(markdown)
|
||||
if trimmed == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
words := len(strings.Fields(trimmed))
|
||||
if words > 0 {
|
||||
return words
|
||||
}
|
||||
|
||||
return utf8.RuneCountInString(trimmed)
|
||||
}
|
||||
|
||||
func stringValue(value interface{}) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
text, ok := value.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return text
|
||||
}
|
||||
Reference in New Issue
Block a user