migrate from github

This commit is contained in:
2026-07-15 22:42:31 +08:00
commit 3ad559f05e
262 changed files with 21637 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
.gitignore
config.example.toml
pkg/gen/
+20
View File
@@ -0,0 +1,20 @@
http_test/
# binaries
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
# debug binaries
__debug_bin*
# config
config.yaml
config.toml
# generated gRPC code
pkg/gen/
+18
View File
@@ -0,0 +1,18 @@
FROM golang:1.24-alpine AS dev
WORKDIR /app
ENV GOPROXY=https://goproxy.cn,direct
RUN apk add --no-cache wget && \
go install github.com/go-delve/delve/cmd/dlv@v1.24.2 && \
wget -O /usr/local/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v0.4.22/grpc_health_probe-linux-amd64 && \
chmod +x /usr/local/bin/grpc_health_probe
COPY go.mod ./
RUN go mod download
EXPOSE 8080
EXPOSE 9090
EXPOSE 2345
CMD ["sh", "-c", "go mod download && go run ./cmd/migrate && go run ./cmd/db_init && go run ./cmd/es_init && dlv debug --headless --listen=:2345 --api-version=2 --accept-multiclient --continue --log ./cmd/main.go"]
+32
View File
@@ -0,0 +1,32 @@
FROM golang:1.24-alpine AS builder
WORKDIR /app
ENV GOPROXY=https://goproxy.cn,direct
RUN apk add --no-cache protobuf
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.5 && \
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1
COPY backend/go.mod backend/go.sum ./backend/
COPY backend ./backend
COPY proto ./proto
WORKDIR /app/backend
RUN go mod download
RUN mkdir -p /app/backend/pkg/gen && \
protoc -I /app/proto \
--go_out=/app/backend/pkg/gen --go_opt=paths=source_relative \
--go-grpc_out=/app/backend/pkg/gen --go-grpc_opt=paths=source_relative \
/app/proto/yoresee_doc/v1/yoresee_doc.proto
RUN go mod tidy
RUN CGO_ENABLED=0 go build -p 1 -o notification_worker.out ./cmd/notification-worker
FROM alpine:latest AS runner
WORKDIR /app
COPY --from=builder /app/backend/notification_worker.out .
CMD ["./notification_worker.out"]
+41
View File
@@ -0,0 +1,41 @@
FROM golang:1.24-alpine AS builder
WORKDIR /app
ENV GOPROXY=https://goproxy.cn,direct
RUN apk add --no-cache protobuf
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.5 && \
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1
COPY backend/go.mod backend/go.sum ./backend/
COPY backend ./backend
COPY proto ./proto
WORKDIR /app/backend
RUN go mod download
RUN mkdir -p /app/backend/pkg/gen && \
protoc -I /app/proto \
--go_out=/app/backend/pkg/gen --go_opt=paths=source_relative \
--go-grpc_out=/app/backend/pkg/gen --go-grpc_opt=paths=source_relative \
/app/proto/yoresee_doc/v1/yoresee_doc.proto
RUN go mod tidy
RUN CGO_ENABLED=0 go build -p 1 -o main.out cmd/main.go && \
CGO_ENABLED=0 go build -p 1 -o migrate.out ./cmd/migrate && \
CGO_ENABLED=0 go build -p 1 -o db_init.out ./cmd/db_init && \
CGO_ENABLED=0 go build -p 1 -o es_init.out ./cmd/es_init
FROM alpine:latest AS runner
WORKDIR /app
COPY --from=builder /app/backend/main.out .
COPY --from=builder /app/backend/migrate.out .
COPY --from=builder /app/backend/db_init.out .
COPY --from=builder /app/backend/es_init.out .
EXPOSE 8080
EXPOSE 9090
CMD ["sh", "-c", "./migrate.out && ./db_init.out && ./es_init.out && ./main.out"]
+32
View File
@@ -0,0 +1,32 @@
FROM golang:1.24-alpine AS builder
WORKDIR /app
ENV GOPROXY=https://goproxy.cn,direct
RUN apk add --no-cache protobuf
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.5 && \
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1
COPY backend/go.mod backend/go.sum ./backend/
COPY backend ./backend
COPY proto ./proto
WORKDIR /app/backend
RUN go mod download
RUN mkdir -p /app/backend/pkg/gen && \
protoc -I /app/proto \
--go_out=/app/backend/pkg/gen --go_opt=paths=source_relative \
--go-grpc_out=/app/backend/pkg/gen --go-grpc_opt=paths=source_relative \
/app/proto/yoresee_doc/v1/yoresee_doc.proto
RUN go mod tidy
RUN CGO_ENABLED=0 go build -p 1 -o search_sync_worker.out ./cmd/search-sync-worker
FROM alpine:latest AS runner
WORKDIR /app
COPY --from=builder /app/backend/search_sync_worker.out .
CMD ["./search_sync_worker.out"]
+32
View File
@@ -0,0 +1,32 @@
FROM golang:1.24-alpine AS builder
WORKDIR /app
ENV GOPROXY=https://goproxy.cn,direct
RUN apk add --no-cache protobuf
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.5 && \
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1
COPY backend/go.mod backend/go.sum ./backend/
COPY backend ./backend
COPY proto ./proto
WORKDIR /app/backend
RUN go mod download
RUN mkdir -p /app/backend/pkg/gen && \
protoc -I /app/proto \
--go_out=/app/backend/pkg/gen --go_opt=paths=source_relative \
--go-grpc_out=/app/backend/pkg/gen --go-grpc_opt=paths=source_relative \
/app/proto/yoresee_doc/v1/yoresee_doc.proto
RUN go mod tidy
RUN CGO_ENABLED=0 go build -p 1 -o snapshot_worker.out ./cmd/snapshot-worker
FROM alpine:latest AS runner
WORKDIR /app
COPY --from=builder /app/backend/snapshot_worker.out .
CMD ["./snapshot_worker.out"]
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"fmt"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
func createUserInTx(tx *gorm.DB, username, email, password string) error {
logrus.Printf("Creating user %s in transaction...", username)
hashedPwd, err := utils.HashPassword(password)
if err != nil {
return err
}
externalID := utils.GenerateExternalID(utils.ExternalIDContextDocument)
user := model.User{
ExternalID: externalID,
Username: username,
PasswordHash: hashedPwd,
Email: email,
Nickname: username,
Status: 1,
}
var count int64
tx.Model(&model.User{}).Where("email = ?", user.Email).Count(&count)
if count == 0 {
if err := tx.Create(&user).Error; err != nil {
return err
}
logrus.Printf("User %s created successfully in transaction.", username)
} else {
logrus.Printf("User %s already exists in transaction.", username)
}
return nil
}
func createUserWithUsername(tx *gorm.DB, username string) error {
email := fmt.Sprintf("%s@yoresee.cc", username)
password := username
return createUserInTx(tx, username, email, password)
}
func createAdminUserInTx(tx *gorm.DB) error {
logrus.Println("Creating admin user in transaction...")
if err := createUserWithUsername(tx, "admin"); err != nil {
return err
}
logrus.Println("Admin permission granted successfully in transaction.")
return nil
}
+34
View File
@@ -0,0 +1,34 @@
package main
import (
"context"
"github.com/XingfenD/yoresee_doc/internal/constant"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/XingfenD/yoresee_doc/pkg/storage"
)
func initializeConfigInConsul(ctx context.Context) error {
registerModeKey := utils.GenConfigKey(
constant.ConfigKey_First_System,
constant.ConfigKey_Second_Security,
constant.ConfigKey_Third_RegisterMode,
)
if _, ok, err := storage.Consul.Get(ctx, registerModeKey); err != nil {
return err
} else if !ok {
if err := storage.Consul.Set(ctx, registerModeKey, constant.RegisterMode_Invite); err != nil {
return err
}
}
return nil
}
func markDatabaseInitializedInConsul(ctx context.Context) error {
initializedKey := utils.GenConfigKey(
constant.ConfigKey_First_System,
constant.ConfigKey_Second_Database,
constant.ConfigKey_Third_Initialized,
)
return storage.Consul.Set(ctx, initializedKey, constant.Database_Initialized_True)
}
+182
View File
@@ -0,0 +1,182 @@
package main
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type DocumentInput struct {
Title string
Content string
Summary string
ParentID int64
Tags []string
KnowledgeID *int64
}
func createDocumentWithAllTables(tx *gorm.DB, adminUserID int64, input DocumentInput) (*model.Document, error) {
document := model.Document{
ExternalID: utils.GenerateExternalID(utils.ExternalIDContextDocument),
Title: input.Title,
Type: "markdown",
Summary: input.Summary,
ParentID: input.ParentID,
UserID: adminUserID,
Content: input.Content,
Tags: input.Tags,
ViewCount: 0,
EditCount: 0,
KnowledgeID: input.KnowledgeID,
Path: "0",
Depth: 0,
}
if err := tx.Create(&document).Error; err != nil {
return nil, err
}
if err := updateDocumentPathDepth(tx, document.ID, document.ParentID); err != nil {
return nil, err
}
documentVersion := model.DocumentVersion{
DocumentID: document.ID,
Version: 1,
Title: document.Title,
Content: document.Content,
UserID: adminUserID,
ChangeSummary: "Initial version",
}
if err := tx.Create(&documentVersion).Error; err != nil {
return nil, err
}
return &document, nil
}
func initializeDocumentsInTx(tx *gorm.DB) error {
logrus.Println("Initializing default documents in transaction...")
var adminUser model.User
if err := tx.Where("username = ?", "admin").First(&adminUser).Error; err != nil {
return err
}
var count int64
tx.Model(&model.Document{}).Where("title = ?", "欢迎使用 Yoresee Doc").Count(&count)
if count > 0 {
logrus.Println("Default document already exists in transaction.")
return nil
}
contentString := "# 欢迎使用 Yoresee Doc\n\n这是您的第一个文档。Yoresee Doc 是一个功能强大的文档管理系统,支持以下特性:\n\n- 📝 富文本编辑\n- 📁 文档分类管理\n- 🔍 全文搜索\n- 👥 协作编辑\n- 📊 版本控制\n- 🔒 权限管理\n\n## 快速开始\n\n1. 点击左侧菜单创建新文档\n2. 使用编辑器撰写内容\n3. 保存文档并分享给团队成员\n\n祝您使用愉快!"
document := model.Document{
ExternalID: utils.GenerateExternalID(utils.ExternalIDContextDocument),
Title: "欢迎使用 Yoresee Doc",
Type: "markdown",
Summary: "Yoresee Doc 系统欢迎文档",
ParentID: 0,
UserID: adminUser.ID,
Content: contentString,
Tags: []string{"guide", "welcome"},
ViewCount: 0,
EditCount: 0,
Path: "0",
Depth: 0,
}
if err := tx.Create(&document).Error; err != nil {
return err
}
if err := updateDocumentPathDepth(tx, document.ID, document.ParentID); err != nil {
return err
}
documentVersion := model.DocumentVersion{
DocumentID: document.ID,
Version: 1,
Title: document.Title,
Content: contentString,
UserID: adminUser.ID,
ChangeSummary: "Initial version",
}
if err := tx.Create(&documentVersion).Error; err != nil {
return err
}
logrus.Println("Adding child documents to welcome document...")
childDocumentTitles := []string{
"功能介绍",
"使用教程",
"常见问题",
"更新日志",
"联系我们",
}
for _, title := range childDocumentTitles {
childDoc, err := createDocumentWithAllTables(tx, adminUser.ID, DocumentInput{
Title: title,
Content: "# " + title + "\n\n这是" + title + "的详细文档内容。",
Summary: title + "相关文档",
ParentID: document.ID,
Tags: []string{"welcome", title},
KnowledgeID: nil,
})
if err != nil {
logrus.Warnf("Failed to create child document %s: %v", title, err)
continue
}
for k := 1; k <= 2; k++ {
grandchildTitle := title + " - 详情" + string(rune('0'+k))
_, err := createDocumentWithAllTables(tx, adminUser.ID, DocumentInput{
Title: grandchildTitle,
Content: "# " + grandchildTitle + "\n\n这是" + grandchildTitle + "的详细文档内容。",
Summary: grandchildTitle + "相关文档",
ParentID: childDoc.ID,
Tags: []string{"welcome", title, "详情"},
KnowledgeID: nil,
})
if err != nil {
logrus.Warnf("Failed to create grandchild document %s: %v", grandchildTitle, err)
}
}
}
logrus.Println("Child documents added to welcome document successfully")
logrus.Println("Default document created successfully in transaction")
return nil
}
func updateDocumentPathDepth(tx *gorm.DB, docID int64, parentID int64) error {
if parentID == 0 {
return tx.Exec(`
UPDATE document_metas
SET path = (id::text)::ltree, depth = 0
WHERE id = ?
`, docID).Error
}
type pathDepth struct {
Path string
Depth int
}
var parent pathDepth
if err := tx.Model(&model.Document{}).
Select("path, depth").
Where("id = ?", parentID).
Take(&parent).Error; err != nil {
return err
}
return tx.Exec(`
UPDATE document_metas
SET path = (?::ltree) || (id::text)::ltree, depth = ?
WHERE id = ?
`, parent.Path, parent.Depth+1, docID).Error
}
+117
View File
@@ -0,0 +1,117 @@
package main
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
func initializeKnowledgeBasesInTx(tx *gorm.DB) error {
logrus.Println("Initializing default knowledge bases in transaction...")
var adminUser model.User
if err := tx.Where("username = ?", "admin").First(&adminUser).Error; err != nil {
return err
}
var count int64
tx.Model(&model.KnowledgeBase{}).Where("name = ?", "默认知识库").Count(&count)
if count > 0 {
logrus.Println("Default knowledge base already exists in transaction.")
return nil
}
defaultKnowledgeBase := model.KnowledgeBase{
ExternalID: utils.GenerateExternalID(utils.ExternalIDKnowledgeBase),
Name: "默认知识库",
Description: "系统默认创建的知识库,用于存储常用文档和资料",
Cover: "",
CreatorUserID: adminUser.ID,
IsPublic: false,
}
if err := tx.Create(&defaultKnowledgeBase).Error; err != nil {
return err
}
exampleKnowledgeBase := model.KnowledgeBase{
ExternalID: utils.GenerateExternalID(utils.ExternalIDKnowledgeBase),
Name: "示例知识库",
Description: "示例知识库,展示知识库功能",
Cover: "",
CreatorUserID: adminUser.ID,
IsPublic: true,
}
if err := tx.Create(&exampleKnowledgeBase).Error; err != nil {
return err
}
logrus.Println("Adding documents to default knowledge base...")
rootDocumentTitles := []string{
"项目概述",
"系统架构",
"开发规范",
"API文档",
"数据库设计",
"前端开发",
"后端开发",
"测试计划",
"部署指南",
"安全规范",
"用户手册",
}
for _, title := range rootDocumentTitles {
rootDoc, err := createDocumentWithAllTables(tx, adminUser.ID, DocumentInput{
Title: title,
Content: "# " + title + "\n\n这是" + title + "的详细文档内容。",
Summary: title + "相关文档",
ParentID: 0,
Tags: []string{title},
KnowledgeID: &defaultKnowledgeBase.ID,
})
if err != nil {
logrus.Warnf("Failed to create root document %s: %v", title, err)
continue
}
for j := 1; j <= 3; j++ {
childTitle := title + " - 子文档" + string(rune('0'+j))
childDoc, err := createDocumentWithAllTables(tx, adminUser.ID, DocumentInput{
Title: childTitle,
Content: "# " + childTitle + "\n\n这是" + childTitle + "的详细文档内容。",
Summary: childTitle + "相关文档",
ParentID: rootDoc.ID,
Tags: []string{title, "子文档"},
KnowledgeID: &defaultKnowledgeBase.ID,
})
if err != nil {
logrus.Warnf("Failed to create child document %s: %v", childTitle, err)
continue
}
for k := 1; k <= 2; k++ {
grandchildTitle := childTitle + " - 孙文档" + string(rune('0'+k))
_, err := createDocumentWithAllTables(tx, adminUser.ID, DocumentInput{
Title: grandchildTitle,
Content: "# " + grandchildTitle + "\n\n这是" + grandchildTitle + "的详细文档内容。",
Summary: grandchildTitle + "相关文档",
ParentID: childDoc.ID,
Tags: []string{title, "子文档", "孙文档"},
KnowledgeID: &defaultKnowledgeBase.ID,
})
if err != nil {
logrus.Warnf("Failed to create grandchild document %s: %v", grandchildTitle, err)
}
}
}
}
logrus.Println("Documents added to default knowledge base successfully")
logrus.Println("Default knowledge bases created successfully in transaction")
return nil
}
+91
View File
@@ -0,0 +1,91 @@
package main
import (
"context"
"github.com/XingfenD/yoresee_doc/internal/bootstrap"
"github.com/XingfenD/yoresee_doc/internal/constant"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/XingfenD/yoresee_doc/pkg/storage"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
func main() {
initializer := bootstrap.NewInitializer().
InitConfig().
InitPostgres().
InitConsul().
RequireConsulEnabled()
if err := initializer.Err(); err != nil {
logrus.Fatalf("Init db_init failed: %v", err)
}
defer initializer.Shutdown()
if isDatabaseInitialized() {
logrus.Println("Database already initialized, skipping initialization steps")
} else {
if err := initializeDatabaseInTransaction(); err != nil {
logrus.Fatalf("Database initialization failed: %v", err)
}
if err := initializeConfigInConsul(context.Background()); err != nil {
logrus.Fatalf("Consul config initialization failed: %v", err)
}
if err := markDatabaseInitializedInConsul(context.Background()); err != nil {
logrus.Fatalf("Mark database initialized failed: %v", err)
}
logrus.Println("Database initialized successfully")
}
logrus.Println("All initialization tasks completed successfully!")
}
func isDatabaseInitialized() bool {
value, ok, err := storage.Consul.Get(context.Background(), utils.GenConfigKey(
constant.ConfigKey_First_System,
constant.ConfigKey_Second_Database,
constant.ConfigKey_Third_Initialized,
))
if err != nil || !ok {
return false
}
return value == constant.Database_Initialized_True
}
func initializeDatabaseInTransaction() error {
return utils.WithTransaction(storage.DB, func(tx *gorm.DB) error {
if err := initializePermissionsInTx(tx); err != nil {
return err
}
if err := createAdminUserInTx(tx); err != nil {
return err
}
if err := createTestUserIntx(tx); err != nil {
return err
}
if err := initializeDocumentsInTx(tx); err != nil {
return err
}
if err := initializeKnowledgeBasesInTx(tx); err != nil {
return err
}
if err := initializeTemplatesInTx(tx); err != nil {
return err
}
if err := createNormalUserInTx(tx); err != nil {
return err
}
if err := initializeUserGroupsInTx(tx); err != nil {
return err
}
return nil
})
}
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"strconv"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
func createTestUserIntx(tx *gorm.DB) error {
logrus.Println("Creating test user in transaction...")
return createUserWithUsername(tx, "test")
}
func createNormalUserInTx(tx *gorm.DB) error {
logrus.Println("Creating 100 users in transaction...")
for i := 1; i <= 20; i++ {
username := "user" + strconv.Itoa(i)
if err := createUserWithUsername(tx, username); err != nil {
return err
}
var user model.User
if err := tx.Where("username = ?", username).First(&user).Error; err != nil {
return err
}
userNum := strconv.Itoa(i)
logrus.Printf("User%s created successfully with ID: %s in transaction.\n", userNum, user.ExternalID)
logrus.Printf("Creating knowledge base for user%s...\n", userNum)
kbExternalID := utils.GenerateExternalID(utils.ExternalIDKnowledgeBase)
knowledgeBase := model.KnowledgeBase{
ExternalID: kbExternalID,
Name: "User" + userNum + "'s Knowledge Base",
Description: "Knowledge base created by user" + userNum,
CreatorUserID: user.ID,
IsPublic: false,
}
var kbCount int64
tx.Model(&model.KnowledgeBase{}).Where("name = ?", knowledgeBase.Name).Count(&kbCount)
if kbCount == 0 {
if err := tx.Create(&knowledgeBase).Error; err != nil {
return err
}
logrus.Printf("Knowledge base created successfully for user%s: %s\n", userNum, knowledgeBase.Name)
} else {
logrus.Printf("Knowledge base for user%s already exists.\n", userNum)
}
}
return nil
}
+22
View File
@@ -0,0 +1,22 @@
package main
import (
"gorm.io/gorm"
)
func initializePermissionsInTx(tx *gorm.DB) error {
// logrus.Println("Starting permission initialization in transaction...")
// adminSubject := model.Subject{
// ID: "1",
// Type: model.SubjectTypeUser,
// }
// if err := tx.Where("id = ?", adminSubject.ID).FirstOrCreate(&adminSubject).Error; err != nil {
// return err
// }
// logrus.Println("Admin subject created successfully in transaction.")
// logrus.Println("Permission initialization completed successfully in transaction.")
return nil
}
+141
View File
@@ -0,0 +1,141 @@
package main
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type templateSeed struct {
Name string
Description string
Content string
Scope string
KnowledgeBaseID *int64
Tags []string
}
func initializeTemplatesInTx(tx *gorm.DB) error {
logrus.Println("Initializing default templates in transaction...")
var adminUser model.User
if err := tx.Where("username = ?", "admin").First(&adminUser).Error; err != nil {
return err
}
var defaultKB model.KnowledgeBase
if err := tx.Where("name = ?", "默认知识库").First(&defaultKB).Error; err != nil {
return err
}
seeds := []templateSeed{
{
Name: "个人周报模板",
Description: "用于个人每周工作复盘与计划",
Content: `# 个人周报
## 本周完成
-
## 问题与风险
-
## 下周计划
- `,
Scope: "private",
Tags: []string{"个人", "周报"},
},
{
Name: "会议纪要模板",
Description: "通用会议记录模板",
Content: `# 会议纪要
## 会议信息
- 时间:
- 参与人:
## 讨论内容
-
## 行动项
- [ ] `,
Scope: "private",
Tags: []string{"个人", "会议"},
},
{
Name: "需求评审模板",
Description: "知识库内需求评审记录模板",
KnowledgeBaseID: &defaultKB.ID,
Scope: "knowledge_base",
Content: `# 需求评审
## 背景
## 目标
## 方案评审
- 优点:
- 风险:
## 结论`,
Tags: []string{"知识库", "需求"},
},
{
Name: "技术方案模板",
Description: "知识库内技术方案沉淀模板",
KnowledgeBaseID: &defaultKB.ID,
Scope: "knowledge_base",
Content: `# 技术方案
## 问题定义
## 方案设计
## 数据模型
## 发布计划
`,
Tags: []string{"知识库", "技术"},
},
}
for _, seed := range seeds {
if err := createTemplateIfNotExists(tx, adminUser.ID, seed); err != nil {
return err
}
}
logrus.Println("Default templates initialized successfully in transaction")
return nil
}
func createTemplateIfNotExists(tx *gorm.DB, userID int64, seed templateSeed) error {
query := tx.Model(&model.Template{}).
Where("user_id = ? AND name = ? AND scope = ?", userID, seed.Name, seed.Scope)
if seed.KnowledgeBaseID == nil {
query = query.Where("knowledge_base_id IS NULL")
} else {
query = query.Where("knowledge_base_id = ?", *seed.KnowledgeBaseID)
}
var count int64
if err := query.Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
template := model.Template{
Name: seed.Name,
Description: seed.Description,
DocumentType: model.DocumentType_Markdown,
Content: seed.Content,
UserID: userID,
Scope: seed.Scope,
KnowledgeBaseID: seed.KnowledgeBaseID,
IsPublic: false,
Tags: seed.Tags,
}
return tx.Create(&template).Error
}
+127
View File
@@ -0,0 +1,127 @@
package main
import (
"errors"
"strings"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type seedUserGroup struct {
Name string
Description string
MemberUsers []string
}
func initializeUserGroupsInTx(tx *gorm.DB) error {
logrus.Println("Creating user groups in transaction...")
adminUser, err := getUserByUsername(tx, "admin")
if err != nil {
return err
}
user2, err := getUserByUsername(tx, "user2")
if err != nil {
return err
}
seedGroups := []seedUserGroup{
{
Name: "Admin Group",
Description: "Group for admin only",
MemberUsers: []string{"admin"},
},
{
Name: "User2 Group",
Description: "Group for user2 only",
MemberUsers: []string{"user2"},
},
{
Name: "Admin & User2 Group",
Description: "Group for admin and user2",
MemberUsers: []string{"admin", "user2"},
},
{
Name: "Empty Group",
Description: "Group with no members",
MemberUsers: []string{},
},
}
for _, seed := range seedGroups {
group, err := findOrCreateGroup(tx, seed.Name, seed.Description, adminUser.ID)
if err != nil {
return err
}
for _, username := range seed.MemberUsers {
var userID int64
switch username {
case "admin":
userID = adminUser.ID
case "user2":
userID = user2.ID
default:
continue
}
if err := ensureMembership(tx, group.ID, userID); err != nil {
return err
}
}
}
logrus.Println("User groups initialized successfully in transaction.")
return nil
}
func getUserByUsername(tx *gorm.DB, username string) (*model.User, error) {
var user model.User
if err := tx.Where("username = ?", username).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func findOrCreateGroup(tx *gorm.DB, name, description string, creatorID int64) (*model.UserGroupMeta, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, gorm.ErrInvalidValue
}
var group model.UserGroupMeta
if err := tx.Where("name = ?", name).First(&group).Error; err == nil {
return &group, nil
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
group = model.UserGroupMeta{
ExternalID: utils.GenerateExternalID(utils.ExternalIDContextUserGroup),
Name: name,
Description: strings.TrimSpace(description),
CreatorID: creatorID,
}
if err := tx.Create(&group).Error; err != nil {
return nil, err
}
return &group, nil
}
func ensureMembership(tx *gorm.DB, groupID, userID int64) error {
var count int64
if err := tx.Model(&model.MembershipRelation{}).
Where("type = ? AND membership_id = ? AND user_id = ?", model.MembershipType_UserGroup, groupID, userID).
Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
return tx.Create(&model.MembershipRelation{
Type: model.MembershipType_UserGroup,
MembershipID: groupID,
UserID: userID,
}).Error
}
+118
View File
@@ -0,0 +1,118 @@
package main
import (
"context"
"errors"
"github.com/XingfenD/yoresee_doc/internal/bootstrap"
"github.com/XingfenD/yoresee_doc/internal/config"
"github.com/XingfenD/yoresee_doc/internal/constant"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/internal/search"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/XingfenD/yoresee_doc/pkg/storage"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
const reindexBatchSize = 200
var errESInitDisabled = errors.New("es_init disabled")
func main() {
initializer := bootstrap.NewInitializer().
InitConfig().
Check("check elasticsearch enabled", func() error {
if config.GlobalConfig == nil || !config.GlobalConfig.Elasticsearch.Enabled {
return errESInitDisabled
}
return nil
}).
InitPostgres().
InitConsul().
RequireConsulEnabled().
InitElasticsearch()
if err := initializer.Err(); err != nil {
if errors.Is(err, errESInitDisabled) {
logrus.Println("Elasticsearch disabled, skip es_init")
return
}
logrus.Fatalf("Init es_init failed: %v", err)
}
defer initializer.Shutdown()
ctx := context.Background()
indexName := search.DocumentIndexName()
initialized := isESInitialized(ctx)
exists, err := storage.ES.IndexExists(ctx, indexName)
if err != nil {
logrus.Fatalf("Check Elasticsearch index failed, index=%s, err=%v", indexName, err)
}
if initialized && exists {
logrus.Printf("Elasticsearch already initialized, index=%s", indexName)
return
}
if !exists {
if err := storage.ES.CreateIndex(ctx, indexName, search.BuildDocumentIndexMapping()); err != nil {
logrus.Fatalf("Create Elasticsearch index failed, index=%s, err=%v", indexName, err)
}
logrus.Printf("Elasticsearch index created, index=%s", indexName)
}
if err := reindexDocuments(ctx); err != nil {
logrus.Fatalf("Reindex documents failed: %v", err)
}
if err := markESInitializedInConsul(ctx); err != nil {
logrus.Fatalf("Mark Elasticsearch initialized failed: %v", err)
}
logrus.Println("Elasticsearch initialization completed successfully")
}
func isESInitialized(ctx context.Context) bool {
value, ok, err := storage.Consul.Get(ctx, utils.GenConfigKey(
constant.ConfigKey_First_System,
constant.ConfigKey_Second_ES,
constant.ConfigKey_Third_Initialized,
))
if err != nil || !ok {
return false
}
return value == constant.ES_Initialized_True
}
func markESInitializedInConsul(ctx context.Context) error {
return storage.Consul.Set(
ctx,
utils.GenConfigKey(
constant.ConfigKey_First_System,
constant.ConfigKey_Second_ES,
constant.ConfigKey_Third_Initialized,
),
constant.ES_Initialized_True,
)
}
func reindexDocuments(ctx context.Context) error {
logrus.Println("Start reindexing documents to Elasticsearch...")
var docs []model.Document
return storage.DB.
Model(&model.Document{}).
Where("deleted_at IS NULL").
FindInBatches(&docs, reindexBatchSize, func(tx *gorm.DB, batch int) error {
for i := range docs {
doc := docs[i]
if err := search.UpsertDocument(ctx, &doc); err != nil {
return err
}
}
logrus.Printf("Reindex batch done, batch=%d, size=%d", batch, len(docs))
return nil
}).Error
}
+70
View File
@@ -0,0 +1,70 @@
package main
import (
"context"
"net/http"
"time"
"github.com/XingfenD/yoresee_doc/internal/bootstrap"
"github.com/XingfenD/yoresee_doc/internal/config"
"github.com/XingfenD/yoresee_doc/internal/transport/connectserver"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/sirupsen/logrus"
)
func main() {
initializer := bootstrap.NewInitializer().
InitConfig().
InitPostgres().
InitRedis().
InitConsul().
RequireConsulEnabled().
InitMinio().
InitElasticsearchAllowFail().
InitMQ().
InitRepository().
InitService()
if err := initializer.Err(); err != nil {
logrus.Fatalf("Init backend failed: %v", err)
}
grpcServer, grpcWebServer, err := connectserver.Start(
config.GlobalConfig.Server.GrpcPort,
config.GlobalConfig.Server.GrpcWebPort,
initializer.Repositories.DB,
initializer.Repositories.Redis,
initializer.Repositories,
)
if err != nil {
logrus.Fatalf("Start connect servers failed: %v", err)
}
waitForShutdown(initializer, grpcServer, grpcWebServer)
}
func waitForShutdown(initializer *bootstrap.Initializer, grpcServer, grpcWebServer *http.Server) {
signalCtx, stop := utils.ShutdownContext()
defer stop()
<-signalCtx.Done()
logrus.Info("Shutdown signal received, start draining")
connectserver.SetDraining(true)
shutdownServer("grpc-web", grpcWebServer, 10*time.Second)
shutdownServer("grpc", grpcServer, 10*time.Second)
initializer.Shutdown()
}
func shutdownServer(name string, server *http.Server, timeout time.Duration) {
if server == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logrus.Errorf("Shutdown %s server failed: %v", name, err)
}
}
+25
View File
@@ -0,0 +1,25 @@
package main
import (
"github.com/sirupsen/logrus"
"github.com/XingfenD/yoresee_doc/internal/bootstrap"
)
func main() {
initializer := bootstrap.NewInitializer().
InitConfig().
InitPostgres()
if err := initializer.Err(); err != nil {
logrus.Fatalf("Init migrate failed: %v", err)
}
defer initializer.Shutdown()
logrus.Println("Starting migration...")
if err := runMigration(); err != nil {
logrus.Fatalf("Migration failed: %v", err)
}
logrus.Println("Database migration completed successfully")
}
+69
View File
@@ -0,0 +1,69 @@
package main
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/pkg/storage"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
func runMigration() error {
if err := storage.DB.Exec("CREATE EXTENSION IF NOT EXISTS ltree").Error; err != nil {
return err
}
logrus.Println("ltree extension created successfully")
err := storage.DB.AutoMigrate(
&model.Attachment{},
&model.Document{},
&model.DocumentComment{},
&model.DocumentYjsSnapshot{},
&model.DocumentVersion{},
&model.Invitation{},
&model.InvitationRecord{},
&model.KnowledgeBase{},
&model.RecentDocument{},
&model.RecentKnowledgeBase{},
&model.RecentTemplate{},
&model.MembershipRelation{},
&model.Notification{},
&model.OrgNodeMeta{},
&model.Template{},
&model.UserGroupMeta{},
&model.User{},
)
if err != nil {
return err
}
if err := migrateRecentKnowledgeBaseIndex(storage.DB); err != nil {
return err
}
if err := migrateRecentDocumentIndex(storage.DB); err != nil {
return err
}
if err := migrateRecentTemplateIndex(storage.DB); err != nil {
return err
}
if err := dropDocumentCommentQuote(storage.DB); err != nil {
return err
}
return nil
}
func migrateRecentKnowledgeBaseIndex(db *gorm.DB) error {
return db.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_recent_kb_user_kb ON recent_knowledge_bases (user_id, knowledge_base_id)").Error
}
func migrateRecentTemplateIndex(db *gorm.DB) error {
return db.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_recent_tpl_user_tpl ON recent_templates (user_id, template_id)").Error
}
func migrateRecentDocumentIndex(db *gorm.DB) error {
return db.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_recent_doc_user_doc ON recent_documents (user_id, document_id)").Error
}
func dropDocumentCommentQuote(db *gorm.DB) error {
return db.Exec("ALTER TABLE document_comments DROP COLUMN IF EXISTS quote").Error
}
+79
View File
@@ -0,0 +1,79 @@
package main
import (
"context"
"strings"
"time"
"github.com/XingfenD/yoresee_doc/internal/bootstrap"
"github.com/XingfenD/yoresee_doc/internal/domain_event"
"github.com/XingfenD/yoresee_doc/internal/dto"
"github.com/XingfenD/yoresee_doc/internal/service/mq_service"
"github.com/XingfenD/yoresee_doc/internal/service/notification_service"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/XingfenD/yoresee_doc/pkg/mq"
"github.com/sirupsen/logrus"
)
func main() {
initializer := bootstrap.NewInitializer().
InitConfig().
InitPostgres().
InitRedis().
InitMQ().
InitRepository().
InitService()
if err := initializer.Err(); err != nil {
logrus.Fatalf("Init notification-worker failed: %v", err)
}
topic := domain_event.NotificationCreateTopic()
group := utils.GetEnvVar("NOTIFICATION_MQ_GROUP", "notification-worker")
go func() {
if err := mq_service.MQSvc.Consume(
context.Background(),
mq.BackendRabbitMQ,
mq.ConsumeOptions{
Topic: topic,
Mode: mq.ConsumeModeGroup,
Group: group,
AutoAck: false,
OnError: mq.ErrorActionRequeue,
},
func(ctx context.Context, message mq.Message) error {
return handleNotificationEvent(ctx, message.Body)
},
); err != nil {
logrus.Fatalf("Subscribe failed: %v", err)
}
}()
initializer.ShutdownOnSignal(500 * time.Millisecond)
}
func handleNotificationEvent(ctx context.Context, data []byte) error {
payload := strings.TrimSpace(string(data))
if payload == "" {
return nil
}
evt, err := domain_event.DecodeNotificationCreateEvent(data)
if err != nil {
logrus.Errorf("parse notification event failed: %v", err)
return nil
}
req := &dto.CreateNotificationRequest{
ReceiverExternalIDs: evt.ReceiverExternalIDs,
Type: evt.Type,
Title: evt.Title,
Content: evt.Content,
PayloadJSON: evt.PayloadJSON,
}
if err := notification_service.NotificationSvc.CreateNotifications(req); err != nil {
logrus.Errorf("create notifications failed: %v", err)
return err
}
return nil
}
+100
View File
@@ -0,0 +1,100 @@
package main
import (
"context"
"errors"
"time"
"github.com/XingfenD/yoresee_doc/internal/bootstrap"
"github.com/XingfenD/yoresee_doc/internal/config"
"github.com/XingfenD/yoresee_doc/internal/domain_event"
"github.com/XingfenD/yoresee_doc/internal/repository/document_repo"
"github.com/XingfenD/yoresee_doc/internal/search"
"github.com/XingfenD/yoresee_doc/internal/service/mq_service"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/XingfenD/yoresee_doc/pkg/mq"
"github.com/sirupsen/logrus"
)
var errSearchSyncWorkerDisabled = errors.New("search-sync-worker disabled")
func main() {
initializer, err := initSearchSyncWorker()
if err != nil {
if errors.Is(err, errSearchSyncWorkerDisabled) {
logrus.Println("Elasticsearch disabled, skip search-sync-worker")
return
}
logrus.Fatalf("Init search-sync-worker failed: %v", err)
}
backend := mq.BackendRabbitMQ
topic := domain_event.DocumentSyncTopic()
group := utils.GetEnvVar("SEARCH_SYNC_MQ_GROUP", "search-sync-worker")
logrus.Infof("Search sync worker started: backend=%s topic=%s group=%s", backend, topic, group)
go func() {
if err := mq_service.MQSvc.Consume(
context.Background(),
backend,
mq.ConsumeOptions{
Topic: topic,
Mode: mq.ConsumeModeGroup,
Group: group,
AutoAck: false,
OnError: mq.ErrorActionRequeue,
},
func(ctx context.Context, message mq.Message) error {
return handleDocumentEvent(ctx, message.Body, initializer.Repositories.Document)
},
); err != nil {
logrus.Fatalf("Subscribe search sync topic failed: %v", err)
}
}()
initializer.ShutdownOnSignal(500 * time.Millisecond)
}
func initSearchSyncWorker() (*bootstrap.Initializer, error) {
initializer := bootstrap.NewInitializer().
InitConfig().
Check("check elasticsearch enabled", func() error {
if config.GlobalConfig == nil || !config.GlobalConfig.Elasticsearch.Enabled {
return errSearchSyncWorkerDisabled
}
return nil
}).
InitPostgres().
InitRedis().
InitElasticsearch().
InitMQ().
InitRepository()
return initializer, initializer.Err()
}
func handleDocumentEvent(ctx context.Context, data []byte, docRepo *document_repo.DocumentRepository) error {
evt, err := domain_event.DecodeDocumentSyncEvent(data)
if err != nil {
logrus.Warnf("Parse search sync event failed: %v", err)
return nil
}
switch evt.Action {
case domain_event.DocumentActionUpsert:
doc, err := docRepo.GetByExternalID(evt.ExternalID).Exec(ctx)
if err != nil {
logrus.Warnf("Search sync load document failed, external_id=%s, err=%v", evt.ExternalID, err)
return err
}
if err := search.UpsertDocument(ctx, doc); err != nil {
logrus.Warnf("Search sync upsert failed, external_id=%s, err=%v", evt.ExternalID, err)
return err
}
logrus.Infof("Search sync upsert success, external_id=%s", evt.ExternalID)
case domain_event.DocumentActionDelete:
// reserved for future hard-delete sync.
default:
logrus.Warnf("Search sync skip unknown action, action=%s, external_id=%s", evt.Action, evt.ExternalID)
}
return nil
}
+46
View File
@@ -0,0 +1,46 @@
package main
import (
"net/http"
"sync"
"time"
"github.com/XingfenD/yoresee_doc/internal/bootstrap"
"github.com/XingfenD/yoresee_doc/internal/service/mq_service"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/XingfenD/yoresee_doc/pkg/constant"
"github.com/XingfenD/yoresee_doc/pkg/key"
"github.com/XingfenD/yoresee_doc/pkg/mq"
"github.com/sirupsen/logrus"
)
func main() {
initializer := bootstrap.NewInitializer().
InitConfig().
InitPostgres().
InitRedis().
InitElasticsearchAllowFail().
InitMQ().
InitRepository().
InitService()
if err := initializer.Err(); err != nil {
logrus.Fatalf("Init snapshot-worker failed: %v", err)
}
w := &Worker{
collabCoreHTTP: utils.GetEnvVar("COLLAB_CORE_HTTP", "http://collab-core:1234"),
mqGroup: utils.GetEnvVar("SNAPSHOT_MQ_GROUP", "snapshot-worker"),
topic: constant.DirtyDocTopicDefault,
mqBackend: mq.BackendRabbitMQ,
client: &http.Client{Timeout: 10 * time.Second},
dirtySetKey: key.KeyCollabDirtyDocSet(),
inFlight: &sync.Map{},
}
logrus.Infof("Snapshot worker started: topic=%s group=%s collabCore=%s", w.topic, w.mqGroup, w.collabCoreHTTP)
go w.runMQConsumer(mq_service.MQSvc)
go w.runScanLoop()
initializer.ShutdownOnSignal(0)
}
+29
View File
@@ -0,0 +1,29 @@
package main
import (
"strings"
"github.com/bytedance/sonic"
)
type dirtyDocMessage struct {
DocID string `json:"doc_id"`
DocId string `json:"docId"`
}
func parseDocID(data []byte) string {
payload := strings.TrimSpace(string(data))
if payload == "" {
return ""
}
var msg dirtyDocMessage
if err := sonic.Unmarshal(data, &msg); err == nil {
if msg.DocID != "" {
return msg.DocID
}
if msg.DocId != "" {
return msg.DocId
}
}
return payload
}
+139
View File
@@ -0,0 +1,139 @@
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/XingfenD/yoresee_doc/internal/domain_event"
"github.com/XingfenD/yoresee_doc/internal/service/document_service"
"github.com/XingfenD/yoresee_doc/internal/status"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/XingfenD/yoresee_doc/pkg/key"
"github.com/XingfenD/yoresee_doc/pkg/storage"
"github.com/bytedance/sonic"
"github.com/sirupsen/logrus"
)
func (w *Worker) snapshotDoc(ctx context.Context, docID string, force bool) error {
if _, loaded := w.inFlight.LoadOrStore(docID, struct{}{}); loaded {
return nil
}
defer w.inFlight.Delete(docID)
logrus.Infof("Snapshot start docId=%s force=%v", docID, force)
docType, err := document_service.DocumentSvc.GetDocumentTypeByExternalID(ctx, docID)
if err != nil {
if errors.Is(err, status.StatusDocumentNotFound) {
logrus.Infof("Snapshot doc not found docId=%s, cleaning up Redis", docID)
cleanupCollabKeys(ctx, docID)
return nil
}
logrus.Errorf("Snapshot get doc type failed docId=%s err=%v", docID, err)
return err
}
state, content, err := w.fetchSnapshot(ctx, docID, string(docType))
if err != nil {
logrus.Errorf("Snapshot fetch failed docId=%s err=%v", docID, err)
return err
}
if len(state) == 0 {
logrus.Infof("Snapshot empty docId=%s", docID)
return nil
}
logrus.Infof("Snapshot fetched docId=%s bytes=%d", docID, len(state))
contentChanged, err := document_service.DocumentSvc.SaveDocumentSnapshotAndContent(ctx, docID, state, content)
if err != nil {
logrus.Errorf("Snapshot save failed docId=%s err=%v", docID, err)
return err
}
if contentChanged {
if pubErr := domain_event.PublishDocumentUpsertEvent(ctx, docID); pubErr != nil {
logrus.Warnf("Snapshot publish search sync event failed docId=%s err=%v", docID, pubErr)
}
}
if err := flushCollabKeys(ctx, docID, state); err != nil {
logrus.Errorf("Snapshot redis flush failed docId=%s err=%v", docID, err)
return err
}
if force {
logrus.Infof("Snapshot saved (mq) for %s", docID)
} else {
logrus.Infof("Snapshot saved (scan) for %s", docID)
}
return nil
}
func (w *Worker) fetchSnapshot(ctx context.Context, docID, docType string) (state []byte, content string, err error) {
url := fmt.Sprintf("%s/internal/yjs/doc-snapshot/%s?type=%s", strings.TrimRight(w.collabCoreHTTP, "/"), docID, docType)
resp, err := w.client.Get(url)
if err != nil {
return nil, "", err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, "", nil
}
if resp.StatusCode != http.StatusOK {
return nil, "", fmt.Errorf("unexpected status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, "", err
}
var payload struct {
State string `json:"state"`
Content string `json:"content"`
}
if err := sonic.Unmarshal(body, &payload); err != nil {
return nil, "", err
}
if payload.State == "" {
return nil, payload.Content, nil
}
state, err = utils.DecodeBase64(payload.State)
if err != nil {
return nil, "", err
}
return state, payload.Content, nil
}
func flushCollabKeys(ctx context.Context, docID string, state []byte) error {
rdb := storage.GetRedis()
if rdb == nil {
return nil
}
updatesKey := key.KeyCollabDocUpdates(docID)
pipe := rdb.TxPipeline()
pipe.Del(ctx, updatesKey)
pipe.RPush(ctx, updatesKey, state)
pipe.SRem(ctx, key.KeyCollabDirtyDocSet(), docID)
_, err := pipe.Exec(ctx)
return err
}
func cleanupCollabKeys(ctx context.Context, docID string) {
rdb := storage.GetRedis()
if rdb == nil {
return
}
pipe := rdb.TxPipeline()
pipe.SRem(ctx, key.KeyCollabDirtyDocSet(), docID)
pipe.Del(ctx, key.KeyCollabDocUpdates(docID))
pipe.Del(ctx, key.KeyCollabRoom(docID))
if _, err := pipe.Exec(ctx); err != nil {
logrus.Warnf("Snapshot cleanup Redis failed docId=%s err=%v", docID, err)
}
}
+106
View File
@@ -0,0 +1,106 @@
package main
import (
"context"
"net/http"
"sync"
"time"
"github.com/XingfenD/yoresee_doc/internal/service/mq_service"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/XingfenD/yoresee_doc/pkg/key"
"github.com/XingfenD/yoresee_doc/pkg/mq"
"github.com/XingfenD/yoresee_doc/pkg/storage"
"github.com/sirupsen/logrus"
)
type Worker struct {
collabCoreHTTP string
mqGroup string
topic string
mqBackend mq.Backend
client *http.Client
dirtySetKey string
inFlight *sync.Map
}
func (w *Worker) runMQConsumer(mqSvc *mq_service.MQService) {
err := mqSvc.Consume(
context.Background(),
w.mqBackend,
mq.ConsumeOptions{
Topic: w.topic,
Mode: mq.ConsumeModeGroup,
Group: w.mqGroup,
AutoAck: false,
OnError: mq.ErrorActionRequeue,
},
func(ctx context.Context, message mq.Message) error {
docID := parseDocID(message.Body)
if docID == "" {
return nil
}
return w.snapshotDoc(ctx, docID, true)
},
)
if err != nil {
logrus.Fatalf("MQ consumer failed: %v", err)
}
}
func (w *Worker) runScanLoop() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for range ticker.C {
w.scanAndSnapshot()
}
}
func (w *Worker) scanAndSnapshot() {
ctx := context.Background()
rdb := storage.GetRedis()
if rdb == nil {
return
}
docIDs, err := rdb.SMembers(ctx, w.dirtySetKey).Result()
if err != nil {
logrus.Errorf("Scan dirty docs failed: %v", err)
return
}
candidates := filterCandidates(ctx, docIDs)
logrus.Infof("Scan dirty docs: candidates=%d", len(candidates))
if len(candidates) > 0 {
logrus.Infof("Dirty doc candidates: %v", candidates)
}
for _, docID := range candidates {
_ = w.snapshotDoc(ctx, docID, false)
}
}
func filterCandidates(ctx context.Context, docIDs []string) []string {
now := time.Now().UnixMilli()
candidates := make([]string, 0, len(docIDs))
for _, docID := range docIDs {
if docID == "" {
continue
}
lastStr, err := storage.GetRedis().Get(ctx, key.KeyCollabRoom(docID)).Result()
if err != nil {
logrus.Infof("Dirty doc %s skipped: room key missing", docID)
continue
}
last, err := utils.ParseInt64(lastStr)
if err != nil {
logrus.Infof("Dirty doc %s skipped: invalid room timestamp %s", docID, lastStr)
continue
}
if now-last < 10_000 {
logrus.Infof("Dirty doc %s skipped: lastEditAgo=%dms", docID, now-last)
continue
}
candidates = append(candidates, docID)
}
return candidates
}
+87
View File
@@ -0,0 +1,87 @@
[server]
grpc_port = ${BACKEND_GRPC_PORT}
grpc_web_port = ${BACKEND_GRPC_WEB_PORT}
read_timeout = 10
write_timeout = 10
[database]
host = "${POSTGRES_HOST}"
port = ${POSTGRES_PORT}
user = "${POSTGRES_USER}"
password = "${POSTGRES_PASSWORD}"
name = "${POSTGRES_DB}"
# max_idle_conns = 10
# max_open_conns = 100
# conn_max_lifetime = 3600
# charset = "utf8mb4"
[redis]
host = "${REDIS_HOST}"
port = ${REDIS_PORT}
password = "${REDIS_PASSWORD}"
db = ${REDIS_DB}
timeout = 5
[consul]
enabled = true
address = "${CONSUL_HOST}:${CONSUL_PORT}"
scheme = "http"
token = "${CONSUL_ROOT_TOKEN}"
datacenter = ""
prefix = "${CONSUL_PREFIX}"
[minio]
endpoint = "${MINIO_HOST}:${MINIO_API_PORT}"
access_key = "${MINIO_ROOT_USER}"
secret_key = "${MINIO_ROOT_PASSWORD}"
bucket = "${MINIO_BUCKET}"
use_ssl = false
timeout = 30
[elasticsearch]
enabled = ${ELASTICSEARCH_ENABLED}
addresses = ["http://${ELASTICSEARCH_HOST}:${ELASTICSEARCH_PORT}"]
username = "${ELASTICSEARCH_USERNAME}"
password = "${ELASTICSEARCH_PASSWORD}"
index_prefix = "${ELASTICSEARCH_INDEX_PREFIX}"
timeout = 5
[mq_config]
# [backend.mq_config.redis]
[mq_config.rabbitmq]
url = "amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@${RABBITMQ_HOST}:${RABBITMQ_AMQP_PORT}/"
[backend]
system_name = "${SYSTEM_NAME}"
internal_rpc_key = "${BACKEND_INTERNAL_RPC_KEY}"
[backend.jwt]
secret = "${JWT_SECRET}"
expire = 86400 # 24 hours
refresh_expire = 604800 # 7 days
[backend.log]
level = "debug"
# Optional: silent/error/warn/info. When empty, follows backend.log.level.
# gorm_log_level = "warn"
file = "logs/app.log"
console = true
file_log = true
[backend.document]
max_size = 10
allowed_extensions = [".md", ".txt", ".pdf", ".doc", ".docx"]
storage_path = "uploads"
[backend.security]
password_hash_cost = 12
max_login_attempts = 5
login_lock_duration = 30
[backend.security.cors]
allowed_origins = ["*"]
allowed_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allowed_headers = ["Origin", "Content-Type", "Authorization"]
allow_credentials = true
max_age = 86400
+74
View File
@@ -0,0 +1,74 @@
module github.com/XingfenD/yoresee_doc
go 1.24.1
require (
connectrpc.com/connect v1.19.1
github.com/bwmarrin/snowflake v0.3.0
github.com/bytedance/gg v1.1.0
github.com/bytedance/sonic v1.14.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/improbable-eng/grpc-web v0.15.0
github.com/minio/minio-go/v7 v7.0.98
github.com/rabbitmq/amqp091-go v1.10.0
github.com/redis/go-redis/v9 v9.18.0
github.com/sirupsen/logrus v1.9.4
github.com/spf13/viper v1.21.0
golang.org/x/crypto v0.46.0
golang.org/x/net v0.48.0
golang.org/x/sync v0.19.0
google.golang.org/grpc v1.79.2
google.golang.org/protobuf v1.36.10
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
require (
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/cors v1.7.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
nhooyr.io/websocket v1.8.6 // indirect
)
+652
View File
@@ -0,0 +1,652 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/bytedance/gg v1.1.0 h1:FSKRxOZeN30w7h6snEbHxzgVMUV7+Xu4gc/Lz1cmBFw=
github.com/bytedance/gg v1.1.0/go.mod h1:MeGhXyy5K20hNAU9GkMM51sXdm/lsqdU0CxwIiGvZpo=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I=
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ=
github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76/go.mod h1:x5OoJHDHqxHS801UIuhqGl6QdSAEJvtausosHSdazIo=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k=
nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
+67
View File
@@ -0,0 +1,67 @@
package auth
import (
"fmt"
"time"
"github.com/XingfenD/yoresee_doc/internal/config"
"github.com/golang-jwt/jwt/v5"
)
type JWTValidator struct{}
func (v *JWTValidator) Validate(tokenString string) (*Claims, error) {
return ParseToken(tokenString)
}
func (v *JWTValidator) IsExpired(claims *Claims) bool {
return time.Now().After(claims.ExpiresAt.Time)
}
type Claims struct {
ExternalID string `json:"external_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
func GenerateToken(externalID string, username string) (string, error) {
cfg := config.GlobalConfig.Backend.Jwt
now := time.Now()
expireTime := now.Add(time.Duration(cfg.Expire) * time.Second)
claims := Claims{
ExternalID: externalID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expireTime),
Issuer: "doc_manager",
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString([]byte(cfg.Secret))
if err != nil {
return "", err
}
if err := StoreJWTToken(externalID, token, time.Until(expireTime)); err != nil {
return "", fmt.Errorf("store jwt token failed: %w", err)
}
return token, nil
}
func ParseToken(token string) (*Claims, error) {
cfg := config.GlobalConfig.Backend.Jwt
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(cfg.Secret), nil
})
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}
+102
View File
@@ -0,0 +1,102 @@
package auth
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"github.com/XingfenD/yoresee_doc/internal/status"
"github.com/XingfenD/yoresee_doc/pkg/key"
"github.com/XingfenD/yoresee_doc/pkg/storage"
"github.com/redis/go-redis/v9"
)
func hashToken(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}
func StoreJWTToken(userExternalID, token string, ttl time.Duration) error {
if storage.KVS == nil {
return status.StatusRedisNotInitialized
}
if ttl <= 0 {
return status.GenErrWithCustomMsg(status.StatusInternalParamsError, "invalid jwt ttl")
}
ctx := context.Background()
tokenHash := hashToken(token)
pipe := storage.KVS.TxPipeline()
pipe.Set(ctx, key.KeyJWTActiveToken(tokenHash), userExternalID, ttl)
pipe.SAdd(ctx, key.KeyJWTUserTokenSet(userExternalID), tokenHash)
pipe.Expire(ctx, key.KeyJWTUserTokenSet(userExternalID), ttl)
if _, err := pipe.Exec(ctx); err != nil {
return fmt.Errorf("persist jwt token failed: %w", err)
}
return nil
}
func ValidateJWTTokenInRedis(userExternalID, token string) error {
if storage.KVS == nil {
return status.StatusRedisNotInitialized
}
ctx := context.Background()
tokenHash := hashToken(token)
blacklisted, err := storage.KVS.Exists(ctx, key.KeyJWTBlacklistToken(tokenHash)).Result()
if err != nil {
return fmt.Errorf("query jwt blacklist failed: %w", err)
}
if blacklisted > 0 {
return status.GenErrWithCustomMsg(status.StatusTokenInvalid, "token is blacklisted")
}
storedUserExternalID, err := storage.KVS.Get(ctx, key.KeyJWTActiveToken(tokenHash)).Result()
if err != nil {
if err == redis.Nil {
return status.GenErrWithCustomMsg(status.StatusTokenInvalid, "token is invalid")
}
return fmt.Errorf("query active jwt token failed: %w", err)
}
if storedUserExternalID != userExternalID {
return status.GenErrWithCustomMsg(status.StatusTokenInvalid, "token user mismatch")
}
return nil
}
func BlacklistUserJWTs(userExternalID string) error {
if storage.KVS == nil {
return status.StatusRedisNotInitialized
}
ctx := context.Background()
tokenHashes, err := storage.KVS.SMembers(ctx, key.KeyJWTUserTokenSet(userExternalID)).Result()
if err != nil {
return fmt.Errorf("list user jwt tokens failed: %w", err)
}
if len(tokenHashes) == 0 {
return nil
}
pipe := storage.KVS.TxPipeline()
for _, tokenHash := range tokenHashes {
activeKey := key.KeyJWTActiveToken(tokenHash)
ttlCmd := storage.KVS.TTL(ctx, activeKey)
ttl, ttlErr := ttlCmd.Result()
if ttlErr != nil && ttlErr != redis.Nil {
return fmt.Errorf("query token ttl failed: %w", ttlErr)
}
if ttl <= 0 {
ttl = 24 * time.Hour
}
pipe.Set(ctx, key.KeyJWTBlacklistToken(tokenHash), "1", ttl)
pipe.Del(ctx, activeKey)
}
pipe.Del(ctx, key.KeyJWTUserTokenSet(userExternalID))
if _, err := pipe.Exec(ctx); err != nil {
return fmt.Errorf("blacklist user jwt tokens failed: %w", err)
}
return nil
}
+274
View File
@@ -0,0 +1,274 @@
package bootstrap
import (
"context"
"fmt"
"sync"
"time"
"github.com/XingfenD/yoresee_doc/internal/config"
"github.com/XingfenD/yoresee_doc/internal/repository"
"github.com/XingfenD/yoresee_doc/internal/service"
"github.com/XingfenD/yoresee_doc/internal/service/mq_service"
"github.com/XingfenD/yoresee_doc/internal/utils"
"github.com/XingfenD/yoresee_doc/pkg/storage"
"github.com/sirupsen/logrus"
)
type Initializer struct {
err error
shutdownHooks []utils.ShutdownHook
hookNames map[string]struct{}
shutdownOnce sync.Once
Repositories *repository.Repositories
}
func NewInitializer() *Initializer {
return &Initializer{
hookNames: make(map[string]struct{}),
}
}
func (i *Initializer) Check(step string, fn func() error) *Initializer {
if i.err != nil {
return i
}
if err := fn(); err != nil {
i.err = fmt.Errorf("%s failed: %w", step, err)
}
return i
}
func (i *Initializer) CheckAllowFail(step string, fn func() error) *Initializer {
if i.err != nil {
return i
}
if err := fn(); err != nil {
logrus.Warnf("%s failed, continue with degraded mode: %v", step, err)
}
return i
}
func (i *Initializer) InitConfig() *Initializer {
return i.Check("config.InitConfig", config.InitConfig)
}
func (i *Initializer) InitPostgres() *Initializer {
i.Check("storage.OpenPostgres", func() error {
dbCfg := config.GlobalConfig.Database
logCfg := config.GlobalConfig.Backend.Log
gormLogLevel := logCfg.GormLogLevel
if gormLogLevel == "" {
gormLogLevel = logCfg.Level
}
db, err := storage.OpenPostgres(&storage.PostgresOptions{
Host: dbCfg.Host,
Port: dbCfg.Port,
User: dbCfg.User,
Password: dbCfg.Password,
Name: dbCfg.Name,
MaxIdleConns: dbCfg.MaxIdleConns,
MaxOpenConns: dbCfg.MaxOpenConns,
GormLogLevel: gormLogLevel,
})
if err != nil {
return err
}
storage.DB = db
return nil
})
return i.addShutdownHook("Postgres", func() error {
if storage.DB == nil {
return nil
}
sqlDB, err := storage.DB.DB()
if err != nil {
return err
}
storage.DB = nil
return sqlDB.Close()
})
}
func (i *Initializer) InitRedis() *Initializer {
i.Check("storage.NewRedisClient", func() error {
cfg := config.GlobalConfig.Redis
client, err := storage.NewRedisClient(&storage.RedisOptions{
Host: cfg.Host,
Port: cfg.Port,
Password: cfg.Password,
DB: cfg.DB,
})
if err != nil {
return err
}
storage.KVS = client
return nil
})
return i.addShutdownHook("Redis", func() error {
if storage.KVS == nil {
return nil
}
client := storage.KVS
storage.KVS = nil
return client.Close()
})
}
func (i *Initializer) InitConsul() *Initializer {
return i.Check("storage.NewConsulClient", func() error {
cfg := config.GlobalConfig.Consul
storage.Consul = storage.NewConsulClient(&storage.ConsulOptions{
Enabled: cfg.Enabled,
Address: cfg.Address,
Scheme: cfg.Scheme,
Token: cfg.Token,
Datacenter: cfg.Datacenter,
Prefix: cfg.Prefix,
})
return nil
})
}
func (i *Initializer) RequireConsulEnabled() *Initializer {
return i.Check("storage.ConsulEnabled", func() error {
if !storage.ConsulEnabled() {
return fmt.Errorf("consul is required but not enabled")
}
return nil
})
}
func (i *Initializer) InitMinio() *Initializer {
return i.Check("storage.NewMinioClient", func() error {
cfg := config.GlobalConfig.Minio
client, err := storage.NewMinioClient(&storage.MinioOptions{
Endpoint: cfg.Endpoint,
AccessKey: cfg.AccessKey,
SecretKey: cfg.SecretKey,
UseSSL: cfg.UseSSL,
})
if err != nil {
return err
}
if err = storage.EnsureBucket(context.Background(), client, cfg.Bucket); err != nil {
return err
}
if err = storage.EnsureBucketPublicRead(context.Background(), client, cfg.Bucket); err != nil {
return err
}
storage.MinioClient = client
return nil
})
}
func (i *Initializer) InitElasticsearch() *Initializer {
i.Check("storage.NewElasticsearchClient", func() error {
cfg := config.GlobalConfig.Elasticsearch
client, err := storage.NewElasticsearchClient(&storage.ElasticsearchOptions{
Enabled: cfg.Enabled,
Addresses: cfg.Addresses,
Username: cfg.Username,
Password: cfg.Password,
Timeout: cfg.Timeout,
})
if err != nil {
return err
}
storage.ES = client
return nil
})
return i.addShutdownHook("Elasticsearch", func() error {
storage.ES = nil
return nil
})
}
func (i *Initializer) InitElasticsearchAllowFail() *Initializer {
i.CheckAllowFail("storage.NewElasticsearchClient", func() error {
cfg := config.GlobalConfig.Elasticsearch
client, err := storage.NewElasticsearchClient(&storage.ElasticsearchOptions{
Enabled: cfg.Enabled,
Addresses: cfg.Addresses,
Username: cfg.Username,
Password: cfg.Password,
Timeout: cfg.Timeout,
})
if err != nil {
return err
}
storage.ES = client
return nil
})
if storage.ES != nil {
i.addShutdownHook("Elasticsearch", func() error {
storage.ES = nil
return nil
})
}
return i
}
func (i *Initializer) InitMQ() *Initializer {
i.Check("mq_service.Init", func() error {
return mq_service.MQSvc.Init(&config.GlobalConfig.MQConfig)
})
return i.addShutdownHook("MQ", mq_service.MQSvc.Close)
}
func (i *Initializer) InitRepository() *Initializer {
return i.Check("repository.NewRepositories", func() error {
i.Repositories = repository.NewRepositories(storage.DB, storage.KVS)
return nil
})
}
func (i *Initializer) InitService() *Initializer {
return i.Check("service.Init", func() error {
return service.Init(config.GlobalConfig, i.Repositories)
})
}
func (i *Initializer) Err() error {
return i.err
}
func (i *Initializer) Shutdown() {
i.shutdownOnce.Do(func() {
if len(i.shutdownHooks) == 0 {
return
}
reversed := make([]utils.ShutdownHook, 0, len(i.shutdownHooks))
for idx := len(i.shutdownHooks) - 1; idx >= 0; idx-- {
reversed = append(reversed, i.shutdownHooks[idx])
}
utils.RunShutdownHooks(reversed...)
})
}
func (i *Initializer) ShutdownOnSignal(delay time.Duration) {
utils.WaitForShutdownSignal()
if delay > 0 {
time.Sleep(delay)
}
i.Shutdown()
}
func (i *Initializer) addShutdownHook(name string, fn func() error) *Initializer {
if i.err != nil || fn == nil || name == "" {
return i
}
if _, exists := i.hookNames[name]; exists {
return i
}
i.hookNames[name] = struct{}{}
i.shutdownHooks = append(i.shutdownHooks, utils.ShutdownHook{
Name: name,
Fn: fn,
})
return i
}
+144
View File
@@ -0,0 +1,144 @@
package cache
import (
"context"
"errors"
"math/rand"
"time"
"github.com/XingfenD/yoresee_doc/internal/status"
"github.com/bytedance/sonic"
"github.com/redis/go-redis/v9"
"golang.org/x/sync/singleflight"
"gorm.io/gorm"
)
type Loader struct {
redis *redis.Client
sf singleflight.Group
}
func (l *Loader) Redis() *redis.Client {
return l.redis
}
func NewLoader(redis *redis.Client) *Loader {
return &Loader{
redis: redis,
}
}
type CacheLoadOperation[T any] struct {
loader *Loader
keyParserMap map[string]Parser[T]
defaultKey *string
baseTTL time.Duration
dbLoader func() (*T, error)
valueType bool
}
func NewCacheLoadOperation[T any](loader *Loader) *CacheLoadOperation[T] {
return &CacheLoadOperation[T]{
loader: loader,
baseTTL: time.Hour,
valueType: false,
keyParserMap: make(map[string]Parser[T]),
}
}
func (op *CacheLoadOperation[T]) WithDefaultKeyAndParser(key string, parser Parser[T]) *CacheLoadOperation[T] {
var defaultParser Parser[T]
op.defaultKey = &key
if parser == nil {
defaultParser = DefaultParser[T]
} else {
defaultParser = parser
}
op.keyParserMap[key] = defaultParser
return op
}
func (op *CacheLoadOperation[T]) WithKeyAndParser(key string, parser Parser[T]) *CacheLoadOperation[T] {
op.keyParserMap[key] = parser
return op
}
func (op *CacheLoadOperation[T]) WithTTL(ttl time.Duration) *CacheLoadOperation[T] {
op.baseTTL = ttl
return op
}
func (op *CacheLoadOperation[T]) WithDBLoader(dbLoader func() (*T, error)) *CacheLoadOperation[T] {
op.dbLoader = dbLoader
return op
}
func (op *CacheLoadOperation[T]) Exec(ctx context.Context) (*T, error) {
if op.defaultKey == nil {
return nil, status.GenErrWithCustomMsg(status.StatusInternalParamsError, "default key is needed")
}
if op.dbLoader == nil {
return nil, status.GenErrWithCustomMsg(status.StatusInternalParamsError, "database loader function is required")
}
// try to load from any of the cache keys
for key, parser := range op.keyParserMap {
val, err := op.loader.redis.Get(ctx, key).Result()
if err == nil {
if val == "null" {
return nil, nil
}
if data, err := parser([]byte(val)); err == nil {
return data, nil
}
}
}
// all caches missed, use singleflight to avoid concurrent db queries
sfKey := *op.defaultKey
v, err, _ := op.loader.sf.Do(sfKey, func() (interface{}, error) {
// double check all caches
for key, parser := range op.keyParserMap {
val, err := op.loader.redis.Get(ctx, key).Result()
if err == nil {
if val == "null" {
return nil, nil
}
if data, err := parser([]byte(val)); err == nil {
return data, nil
}
}
}
// query database
data, err := op.dbLoader()
if err != nil {
// cache empty value for all keys if record not found
if errors.Is(err, gorm.ErrRecordNotFound) {
op.loader.redis.Set(ctx, *op.defaultKey, "null", 5*time.Minute)
}
return nil, err
}
bytes, _ := sonic.Marshal(data)
ttl := op.baseTTL + time.Duration(rand.Intn(600))*time.Second
op.loader.redis.Set(ctx, *op.defaultKey, bytes, ttl)
return data, nil
})
if err != nil {
return nil, err
}
if v == nil {
return nil, nil
}
return v.(*T), nil
}
+27
View File
@@ -0,0 +1,27 @@
package cache
import (
"encoding/json"
"github.com/XingfenD/yoresee_doc/internal/model"
)
type Parser[T any] func(data []byte) (*T, error)
func DefaultParser[T any](data []byte) (*T, error) {
var value T
if err := json.Unmarshal(data, &value); err != nil {
return nil, err
}
return &value, nil
}
var ParseInt64 = DefaultParser[int64]
func ParseIDFromDocument(data []byte) (*int64, error) {
var doc model.Document
if err := json.Unmarshal(data, &doc); err != nil {
return nil, err
}
return &doc.ID, nil
}
+151
View File
@@ -0,0 +1,151 @@
package config
import (
"strings"
"github.com/XingfenD/yoresee_doc/internal/status"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
Minio MinioConfig `mapstructure:"minio"`
Elasticsearch ElasticsearchConfig `mapstructure:"elasticsearch"`
MQConfig MQConfig `mapstructure:"mq_config"`
Consul ConsulConfig `mapstructure:"consul"`
Backend BackendConfig `mapstructure:"backend"`
}
type ServerConfig struct {
GrpcPort int `mapstructure:"grpc_port"`
GrpcWebPort int `mapstructure:"grpc_web_port"`
}
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Name string `mapstructure:"name"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
MaxOpenConns int `mapstructure:"max_open_conns"`
}
type RedisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
type ConsulConfig struct {
Enabled bool `mapstructure:"enabled"`
Address string `mapstructure:"address"`
Scheme string `mapstructure:"scheme"`
Token string `mapstructure:"token"`
Datacenter string `mapstructure:"datacenter"`
Prefix string `mapstructure:"prefix"`
}
type MinioConfig struct {
Endpoint string `mapstructure:"endpoint"`
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
Bucket string `mapstructure:"bucket"`
UseSSL bool `mapstructure:"use_ssl"`
}
type ElasticsearchConfig struct {
Enabled bool `mapstructure:"enabled"`
Addresses []string `mapstructure:"addresses"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
IndexPrefix string `mapstructure:"index_prefix"`
Timeout int `mapstructure:"timeout"`
}
type ClusterRole string
const (
ClusterRoleProducer ClusterRole = "producer"
ClusterRoleConsumer ClusterRole = "consumer"
ClusterRoleHybrid ClusterRole = "hybrid" // producer and consumer
)
type BackendConfig struct {
Jwt JWTConfig `mapstructure:"jwt"`
Log LogConfig `mapstructure:"log"`
Security SecurityConfig `mapstructure:"security"`
SystemName string `mapstructure:"system_name"`
InternalRPCKey string `mapstructure:"internal_rpc_key"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
GormLogLevel string `mapstructure:"gorm_log_level"`
}
type MQConfig struct {
Redis RedisQueueConfig `mapstructure:"redis"`
RabbitMQ RabbitMQQueueConfig `mapstructure:"rabbitmq"`
}
type RedisQueueConfig struct {
}
type RabbitMQQueueConfig struct {
URL string `mapstructure:"url"`
}
type JWTConfig struct {
Secret string `mapstructure:"secret"`
Expire int64 `mapstructure:"expire"`
}
type SecurityConfig struct {
PasswordHashCost int `mapstructure:"password_hash_cost"`
CORS CORSConfig `mapstructure:"cors"`
}
type CORSConfig struct {
AllowedOrigins []string `mapstructure:"allowed_origins"`
AllowedMethods []string `mapstructure:"allowed_methods"`
AllowedHeaders []string `mapstructure:"allowed_headers"`
AllowCredentials bool `mapstructure:"allow_credentials"`
MaxAge int `mapstructure:"max_age"`
}
var GlobalConfig *Config
func InitConfig() error {
viper.SetConfigName("config")
viper.SetConfigType("toml")
viper.AddConfigPath(".")
viper.AddConfigPath("./config")
if err := viper.ReadInConfig(); err != nil {
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "read config failed")
}
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
if err := viper.Unmarshal(&GlobalConfig); err != nil {
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "unmarshal config failed")
}
levelText := strings.TrimSpace(GlobalConfig.Backend.Log.Level)
if levelText == "" {
levelText = "info"
}
level, err := logrus.ParseLevel(strings.ToLower(levelText))
if err != nil {
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "invalid backend.log.level")
}
logrus.SetLevel(level)
return nil
}
+17
View File
@@ -0,0 +1,17 @@
package constant
const (
ConfigKey_First_System string = "system"
)
const (
ConfigKey_Second_Security string = "security"
ConfigKey_Second_Database string = "database"
ConfigKey_Second_ES string = "elasticsearch"
)
const (
ConfigKey_Third_RegisterMode string = "register_mode"
ConfigKey_Third_Initialized string = "initialized"
ConfigKey_Third_RegisterLimit string = "register_limit" // token_bucket_limit
)
+23
View File
@@ -0,0 +1,23 @@
package constant
// system.security.register_mode
const (
RegisterMode_Open string = "open"
RegisterMode_Invite string = "invite"
)
// system.database.initialized
const (
Database_Initialized_True string = "true"
// Database_Initialized_False string = "false"
)
// system.elasticsearch.initialized
const (
ES_Initialized_True string = "true"
)
const (
RegisterLimit_On string = "on"
RegisterLimit_Off string = "off"
)
+27
View File
@@ -0,0 +1,27 @@
package constant
// Notification type constants.
const (
NotificationType_Comment = "comment"
NotificationType_Reply = "reply"
NotificationType_Mention = "mention"
NotificationType_System = "system"
)
// notificationTitles holds default (zh-CN) titles keyed by notification type.
// Replace with a proper i18n lookup once multi-language support is added.
var notificationTitles = map[string]string{
NotificationType_Comment: "收到新评论",
NotificationType_Reply: "回复了你的评论",
NotificationType_Mention: "在评论中提及了你",
NotificationType_System: "系统通知",
}
// GetNotificationTitle returns the default title for the given notification type.
// Falls back to the type string itself if the type is unrecognised.
func GetNotificationTitle(notifType string) string {
if title, ok := notificationTitles[notifType]; ok {
return title
}
return notifType
}
+51
View File
@@ -0,0 +1,51 @@
package domain_event
import (
"context"
"fmt"
"strings"
"github.com/XingfenD/yoresee_doc/pkg/constant"
"github.com/bytedance/sonic"
)
const (
DocumentActionUpsert = "upsert"
DocumentActionDelete = "delete"
)
type DocumentSyncEvent struct {
Action string `json:"action"`
ExternalID string `json:"external_id"`
}
func DocumentSyncTopic() string {
return constant.SearchSyncTopicDefault
}
func PublishDocumentUpsertEvent(ctx context.Context, externalID string) error {
externalID = strings.TrimSpace(externalID)
if externalID == "" {
return fmt.Errorf("external_id is empty")
}
return publishJSONToRabbitMQ(ctx, DocumentSyncTopic(), DocumentSyncEvent{
Action: DocumentActionUpsert,
ExternalID: externalID,
})
}
func DecodeDocumentSyncEvent(data []byte) (*DocumentSyncEvent, error) {
var evt DocumentSyncEvent
if err := sonic.Unmarshal(data, &evt); err != nil {
return nil, err
}
evt.Action = strings.TrimSpace(evt.Action)
evt.ExternalID = strings.TrimSpace(evt.ExternalID)
if evt.Action == "" {
evt.Action = DocumentActionUpsert
}
if evt.ExternalID == "" {
return nil, fmt.Errorf("external_id is empty")
}
return &evt, nil
}
@@ -0,0 +1,42 @@
package domain_event
import (
"context"
"strings"
"github.com/XingfenD/yoresee_doc/pkg/constant"
"github.com/bytedance/sonic"
)
type NotificationCreateEvent struct {
ReceiverExternalIDs []string `json:"receiver_external_ids"`
Type string `json:"type"`
Title string `json:"title"`
Content string `json:"content"`
PayloadJSON string `json:"payload_json"`
}
func NotificationCreateTopic() string {
return constant.NotificationTopicDefault
}
func PublishNotificationCreateEvent(ctx context.Context, evt NotificationCreateEvent) error {
return publishJSONToRabbitMQ(ctx, NotificationCreateTopic(), evt)
}
func DecodeNotificationCreateEvent(data []byte) (*NotificationCreateEvent, error) {
var evt NotificationCreateEvent
if err := sonic.Unmarshal(data, &evt); err != nil {
return nil, err
}
cleanReceiverIDs := make([]string, 0, len(evt.ReceiverExternalIDs))
for _, receiverID := range evt.ReceiverExternalIDs {
receiverID = strings.TrimSpace(receiverID)
if receiverID == "" {
continue
}
cleanReceiverIDs = append(cleanReceiverIDs, receiverID)
}
evt.ReceiverExternalIDs = cleanReceiverIDs
return &evt, nil
}
+20
View File
@@ -0,0 +1,20 @@
package domain_event
import (
"context"
"github.com/XingfenD/yoresee_doc/internal/service/mq_service"
"github.com/XingfenD/yoresee_doc/pkg/mq"
"github.com/bytedance/sonic"
)
func publishJSONToRabbitMQ(ctx context.Context, topic string, payload any) error {
data, err := sonic.Marshal(payload)
if err != nil {
return err
}
return mq_service.MQSvc.Publish(ctx, mq.BackendRabbitMQ, mq.PublishMessage{
Topic: topic,
Body: data,
})
}
+15
View File
@@ -0,0 +1,15 @@
package dto
import "time"
type AttachmentResponse struct {
ExternalID string `json:"external_id"`
DocumentExternalID string `json:"document_external_id"`
Name string `json:"name"`
Size int64 `json:"size"`
MimeType string `json:"mime_type"`
Path string `json:"path"`
URL string `json:"url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+28
View File
@@ -0,0 +1,28 @@
package dto
type CreateCommentRequest struct {
DocumentExternalID string `json:"document_external_id"`
Content string `json:"content"`
ParentExternalID *string `json:"parent_external_id,omitempty"`
AnchorID *string `json:"anchor_id,omitempty"`
Quote *string `json:"quote,omitempty"`
CreatorExternalID string `json:"creator_external_id"`
MentionedUserExternalIDs []string `json:"mentioned_user_external_ids,omitempty"`
}
type ListCommentsRequest struct {
DocumentExternalID string `json:"document_external_id"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type DeleteCommentRequest struct {
ExternalID string `json:"external_id"`
OperatorExternalID string `json:"operator_external_id"`
}
type UpdateCommentRequest struct {
ExternalID string `json:"external_id"`
Content string `json:"content"`
OperatorExternalID string `json:"operator_external_id"`
}
+169
View File
@@ -0,0 +1,169 @@
package dto
import (
"time"
"github.com/XingfenD/yoresee_doc/internal/model"
)
type DocumentType string
const DocumentType_Markdown DocumentType = "markdown"
const DocumentType_Table DocumentType = "table"
const DocumentType_MarkdownSlide DocumentType = "markdown_slide"
const DocumentType_YoreseeRichText DocumentType = "yoresee_rich_text"
type ContainerType string
const ContainerType_Own ContainerType = "own"
const ContainerType_KnowledgeBase ContainerType = "knowledge_base"
type DocumentBase struct {
ExternalID string `json:"external_id"`
Title string `json:"title"`
Type DocumentType `json:"type"`
Summary string `json:"summary"`
IsPublic bool `json:"is_public"`
Tags []string `json:"tags"`
ViewCount int `json:"view_count"`
EditCount int `json:"edit_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type DocumentMetaResponse struct {
DocumentBase
Children []*DocumentMetaResponse `json:"children,omitempty"`
HasChildren bool `json:"hasChildren,omitempty"`
}
type DocumentResponse struct {
DocumentMetaResponse
Content string `json:"content"`
}
func NewDocumentMetaResponseFromModel(doc *model.Document) *DocumentMetaResponse {
response := &DocumentMetaResponse{
DocumentBase: DocumentBase{
ExternalID: doc.ExternalID,
Title: doc.Title,
Type: DocumentType(doc.Type),
Summary: doc.Summary,
IsPublic: doc.IsPublic,
Tags: doc.Tags,
ViewCount: doc.ViewCount,
EditCount: doc.EditCount,
CreatedAt: doc.CreatedAt,
UpdatedAt: doc.UpdatedAt,
},
}
return response
}
func NewDocumentResponseFromModel(doc *model.Document) *DocumentResponse {
if doc == nil {
return &DocumentResponse{}
}
response := &DocumentResponse{
DocumentMetaResponse: DocumentMetaResponse{
DocumentBase: DocumentBase{
ExternalID: doc.ExternalID,
Title: doc.Title,
Type: DocumentType(doc.Type),
Summary: doc.Summary,
IsPublic: doc.IsPublic,
Tags: doc.Tags,
ViewCount: doc.ViewCount,
EditCount: doc.EditCount,
CreatedAt: doc.CreatedAt,
UpdatedAt: doc.UpdatedAt,
},
},
Content: doc.Content,
}
return response
}
// type DirectoryResponse struct {
// ExternalID string `json:"external_id"`
// Title string `json:"title"`
// HasChildren bool `json:"has_children"`
// ParentID string `json:"parent_id"`
// }
type DocumentsListExternalArgs struct {
UserExternalID *string `json:"user_external_id"`
RootDocumentExternalID *string `json:"root_document_external_id"`
KnowledgeExternalID *string `json:"knowledge_external_id"`
}
type DocumentsListFilterArgs struct {
TitleKeyword *string `json:"title_keyword"`
DocType *string `json:"doc_type"`
Tags []string `json:"tags"`
CreateTimeRangeStart *string `json:"create_time_range_start"`
CreateTimeRangeEnd *string `json:"create_time_range_end"`
UpdateTimeRangeStart *string `json:"update_time_range_start"`
UpdateTimeRangeEnd *string `json:"update_time_range_end"`
}
type ListDocumentsBaseArgs struct {
ListOwnDoc bool `json:"list_own_doc"`
DirectoryOnly bool `json:"directory_only"`
}
type ListRecentDocumentsRequest struct {
UserExternalID string `json:"user_external_id"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
Pagination Pagination `json:"pagination"`
}
type RecordRecentDocumentRequest struct {
UserExternalID string `json:"user_external_id"`
DocumentExternalID string `json:"document_external_id"`
}
type ListDocumentsByExternalRequest struct {
ExternalArgs *DocumentsListExternalArgs `json:"external_args"`
ListDocumentsBaseArgs
FilterArgs *DocumentsListFilterArgs `json:"filter_args"`
SortArgs SortArgs `json:"sort_args"`
Pagination Pagination `json:"pagination"`
Options *RecursiveOptions `json:"options"`
}
type CreateDocumentRequest struct {
Title string `json:"title"`
Type DocumentType `json:"type"`
ContainerType ContainerType `json:"container_type"`
IsPublic bool `json:"is_public"`
CreatorExternalID *string `json:"creator_external_id"`
TemplateID *int64 `json:"template_id"`
// optional
ParentExternalID *string `json:"parent_external_id"`
KnowledgeExternalID *string `json:"knowledge_external_id"`
}
type CreateDocumentResponse struct {
ExternalID string `json:"external_id"`
}
type UpdateDocumentRequest struct {
ExternalID string `json:"external_id"`
Title *string `json:"title,omitempty"`
KnowledgeBaseExternalID *string `json:"knowledge_base_external_id,omitempty"`
ParentExternalID *string `json:"parent_external_id,omitempty"`
MoveToContainer *ContainerType `json:"move_to_container,omitempty"`
Content *string `json:"content,omitempty"`
}
type UpdateDocumentMetaRequest struct {
ExternalID string `json:"external_id"`
Title *string `json:"title,omitempty"`
Summary *string `json:"summary,omitempty"`
Tags *[]string `json:"tags,omitempty"`
}
+14
View File
@@ -0,0 +1,14 @@
package dto
type GetDocumentSettingsRequest struct {
ExternalID string `json:"external_id"`
}
type DocumentSettingsResponse struct {
IsPublic bool `json:"is_public"`
}
type UpdateDocumentSettingsRequest struct {
ExternalID string `json:"external_id"`
IsPublic bool `json:"is_public"`
}
+11
View File
@@ -0,0 +1,11 @@
package dto
import "time"
type DocumentVersionResponse struct {
Version int `json:"version"`
Title string `json:"title"`
Content string `json:"content"`
ChangeSummary string `json:"change_summary"`
CreatedAt time.Time `json:"created_at"`
}
+27
View File
@@ -0,0 +1,27 @@
package dto
type Pagination struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
}
func (p *Pagination) Validate() bool {
if p == nil {
return false
}
if p.Page <= 0 || p.PageSize <= 0 {
return false
}
return true
}
type SortArgs struct {
Field string `json:"field"`
Desc bool `json:"desc"`
}
type RecursiveOptions struct {
IncludeChildren bool `json:"include_children"`
Recursive bool `json:"recursive"`
Depth *int `json:"depth"`
}
+67
View File
@@ -0,0 +1,67 @@
package dto
import "time"
type ListInvitationsReq struct {
CreatorID *int64 `json:"creator_id"`
MaxUsedCnt *int64 `json:"max_used_cnt"`
ExpiresAtStart *string `json:"expires_at_start"`
ExpiresAtEnd *string `json:"expires_at_end"`
CreatedAtStart *string `json:"created_at_start"`
CreatedAtEnd *string `json:"created_at_end"`
Disabled *bool `json:"disabled"`
OnlyMine *bool `json:"only_mine"`
Keyword *string `json:"keyword"`
SortArgs SortArgs `json:"sort_args"`
Pagination Pagination `json:"pagination"`
}
type CreateInvitationRequest struct {
CreatorExternalID string `json:"creator_external_id"`
MaxUsedCnt *int64 `json:"max_used_cnt"`
ExpiresAt *time.Time `json:"expires_at"`
Note *string `json:"note"`
}
type UpdateInvitationRequest struct {
Code string `json:"code"`
MaxUsedCnt *int64 `json:"max_used_cnt"`
ExpiresAt *time.Time `json:"expires_at"`
Disabled *bool `json:"disabled"`
Note *string `json:"note"`
}
type DeleteInvitationRequest struct {
Code string `json:"code"`
}
type InvitationResponse struct {
Code string `json:"code"`
CreatedByExternalID string `json:"created_by_external_id"`
CreatedByName string `json:"created_by_name"`
UsedCnt int64 `json:"used_cnt"`
MaxUsedCnt *int64 `json:"max_used_cnt"`
ExpiresAt *time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
Disabled bool `json:"disabled"`
Note *string `json:"note"`
}
type ListInvitationRecordsRequest struct {
Code *string `json:"code"`
Status *string `json:"status"`
UsedAtStart *string `json:"used_at_start"`
UsedAtEnd *string `json:"used_at_end"`
CreatorID *int64 `json:"creator_id"`
OnlyMine *bool `json:"only_mine"`
Keyword *string `json:"keyword"`
Pagination Pagination `json:"pagination"`
}
type InvitationRecordResponse struct {
Code string `json:"code"`
UsedBy string `json:"used_by"`
UsedByExternalID string `json:"used_by_external_id"`
UsedAt time.Time `json:"used_at"`
Status string `json:"status"`
}
+98
View File
@@ -0,0 +1,98 @@
package dto
import (
"time"
"github.com/XingfenD/yoresee_doc/internal/model"
)
type KnowledgeBaseBase struct {
ExternalID string `json:"external_id"`
Name string `json:"name"`
Description string `json:"description"`
Cover string `json:"cover"`
IsPublic bool `json:"is_public"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt time.Time `json:"deleted_at"`
}
type KnowledgeBaseExtend struct {
CreatorUserExternalID string `json:"creator_user_external_id"`
CreatorName string `json:"creator_name"`
DocumentsCount int64 `json:"documents_count"`
}
type KnowledgeBaseResponse struct {
KnowledgeBaseBase
KnowledgeBaseExtend
}
func NewKnowledgeBaseResponseFromModel(kb *model.KnowledgeBase, kbExtend *KnowledgeBaseExtend) *KnowledgeBaseResponse {
response := &KnowledgeBaseResponse{
KnowledgeBaseBase: KnowledgeBaseBase{
ExternalID: kb.ExternalID,
Name: kb.Name,
Description: kb.Description,
Cover: kb.Cover,
IsPublic: kb.IsPublic,
CreatedAt: kb.CreatedAt,
UpdatedAt: kb.UpdatedAt,
DeletedAt: kb.DeletedAt,
},
}
if kbExtend != nil {
response.KnowledgeBaseExtend = KnowledgeBaseExtend{
CreatorUserExternalID: kbExtend.CreatorUserExternalID,
CreatorName: kbExtend.CreatorName,
DocumentsCount: kbExtend.DocumentsCount,
}
}
return response
}
type CreateRecentKnowledgeBaseRequest struct {
UserExternalID string `json:"user_external_id"`
KnowledgeBaseExternalID string `json:"knowledge_base_external_id"`
AssessTime time.Time `json:"assess_time"`
}
type CreateKnowledgeBaseRequest struct {
CreatorExternalID string `json:"creator_external_id"`
Name string `json:"name"`
Description string `json:"description"`
Cover string `json:"cover"`
IsPublic bool `json:"is_public"`
}
type CreateKnowledgeBaseResponse struct {
ExternalID string `json:"external_id"`
}
type ListRecentKnowledgeBasesRequest struct {
UserExternalID string `json:"user_external_id"`
StartTime *time.Time `json:"start_time,omitempty"`
EndTime *time.Time `json:"end_time,omitempty"`
Pagination Pagination `json:"pagination"`
}
type KnowledgeBaseListByExternalRequest struct {
CreatorExternalID string `json:"creator_external_id"`
FilterArgs *KnowledgeBaseListFilterArgs `json:"filter_args"`
SortArgs SortArgs `json:"sort_args"`
Pagination Pagination `json:"pagination"`
}
type KnowledgeBaseGetByExternalIDRequest struct {
KnowledgeBaseExternalID string `json:"knowledge_base_external_id"`
}
type KnowledgeBaseListFilterArgs struct {
IsPublic *bool `json:"is_public"`
NameKeyword *string `json:"name_keyword"`
CreateTimeRangeStart *string `json:"create_time_range_start"`
CreateTimeRangeEnd *string `json:"create_time_range_end"`
UpdateTimeRangeStart *string `json:"update_time_range_start"`
UpdateTimeRangeEnd *string `json:"update_time_range_end"`
}
+47
View File
@@ -0,0 +1,47 @@
package dto
// MembershipType mirrors model.MembershipType to keep DTO layer independent.
type MembershipType int64
const (
MembershipType_UserGroup MembershipType = 1
MembershipType_OrgNode MembershipType = 2
)
type MembershipRelationBase struct {
Type MembershipType `json:"type"`
UserID int64 `json:"user_id"`
MembershipID int64 `json:"membership_id"`
}
type MembershipBase struct {
Type MembershipType `json:"type"`
MembershipExternalID string `json:"membership_external_id"`
MembershipName string `json:"membership_name"`
}
type MembershipBaseRequest struct {
Type MembershipType `json:"type"`
MembershipExternalID string `json:"membership_external_id"`
}
type MembershipMetaResponse struct {
MembershipBase
Description string `json:"description"`
}
type CreateMembershipRelationRequest struct {
MembershipBaseRequest
UserExternalID string `json:"user_external_id"`
}
type MembershipRelationResponse struct {
MembershipBase
UserList []UserBase `json:"user_list"`
}
// type MembershipAuthorityResponse struct {
// MembershipBase
// CreatorExternalID string
// CurrentUserAuthority []model.Permission
// }
+20
View File
@@ -0,0 +1,20 @@
package dto
type CreateNotificationRequest struct {
ReceiverExternalIDs []string `json:"receiver_external_ids"`
Type string `json:"type"`
Title string `json:"title"`
Content string `json:"content"`
PayloadJSON string `json:"payload_json"`
}
type ListNotificationsRequest struct {
UserExternalID string `json:"user_external_id"`
Status *string `json:"status,omitempty"`
Pagination Pagination `json:"pagination"`
}
type MarkNotificationsReadRequest struct {
UserExternalID string `json:"user_external_id"`
ExternalIDs []string `json:"external_ids"`
}
+55
View File
@@ -0,0 +1,55 @@
package dto
type OrgNodeResponse struct {
ExternalID string `json:"external_id"`
ParentExternalID string `json:"parent_external_id"`
Name string `json:"name"`
Path string `json:"path"`
Description string `json:"description"`
CreatorUserExternalID string `json:"creator_user_external_id"`
MemberCount int `json:"member_count"`
Children []*OrgNodeResponse `json:"children,omitempty"`
}
type ListOrgNodesRequest struct {
ParentExternalID string `json:"parent_external_id"`
Keyword *string `json:"keyword"`
Pagination Pagination `json:"pagination"`
IncludeChildren bool `json:"include_children"`
}
type GetOrgNodeRequest struct {
ExternalID string `json:"external_id"`
IncludeChildren bool `json:"include_children"`
}
type CreateOrgNodeRequest struct {
CreatorUserExternalID string `json:"creator_user_external_id"`
ParentExternalID string `json:"parent_external_id"`
Name string `json:"name"`
Description string `json:"description"`
MemberUserExternalIDs []string `json:"member_user_external_ids"`
}
type UpdateOrgNodeRequest struct {
ExternalID string `json:"external_id"`
Name *string `json:"name"`
Description *string `json:"description"`
SyncMembers bool `json:"sync_members"`
MemberUserExternalIDs []string `json:"member_user_external_ids"`
}
type DeleteOrgNodeRequest struct {
ExternalID string `json:"external_id"`
}
type MoveOrgNodeRequest struct {
ExternalID string `json:"external_id"`
NewParentExternalID string `json:"new_parent_external_id"`
}
type ListOrgNodeMembersRequest struct {
ExternalID string `json:"external_id"`
Keyword *string `json:"keyword"`
Pagination Pagination `json:"pagination"`
}
+46
View File
@@ -0,0 +1,46 @@
package dto
import (
"time"
)
// PermissionGrant 权限授予请求
type PermissionGrant struct {
Resource Resource `json:"resource" binding:"required"`
Subject Subject `json:"subject" binding:"required"`
Permissions []string `json:"permissions" binding:"required"`
Scope Scope `json:"scope" binding:"required"`
Priority int `json:"priority"`
IsDeny bool `json:"is_deny"`
ValidFrom *time.Time `json:"valid_from"`
ValidUntil *time.Time `json:"valid_until"`
}
// Resource 资源DTO
type Resource struct {
Type string `json:"type" binding:"required"`
ID string `json:"id" binding:"required"`
Path string `json:"path"`
}
type Subject struct {
Type string `json:"type" binding:"required"`
ID string `json:"id" binding:"required"`
}
type Scope struct {
Type string `json:"type" binding:"required"`
}
type PermissionCheck struct {
Resource Resource `json:"resource" binding:"required"`
Permission string `json:"permission" binding:"required"`
}
type PermissionBatchCheck struct {
Resource Resource `json:"resource" binding:"required"`
Permissions []string `json:"permissions" binding:"required"`
}
type PermissionBatchCheckResponse map[string]bool
type PermissionEffectiveResponse []string
+67
View File
@@ -0,0 +1,67 @@
package dto
import "time"
type TemplateContainer string
const (
TemplateContainerOwn TemplateContainer = "own"
TemplateContainerKnowledgeBase TemplateContainer = "knowledge_base"
TemplateContainerPublic TemplateContainer = "public"
)
type CreateTemplateRequest struct {
UserExternalID string `json:"user_external_id"`
TargetContainer TemplateContainer `json:"target_container"`
KnowledgeBaseExternalID *string `json:"knowledge_base_external_id,omitempty"`
TemplateContent string `json:"template_content"`
Type DocumentType `json:"type"`
}
type UpdateTemplateSettingsRequest struct {
UserExternalID string `json:"user_external_id"`
TemplateID int64 `json:"template_id"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
IsPublic *bool `json:"is_public,omitempty"`
}
type TemplateResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Content string `json:"content"`
Type DocumentType `json:"type"`
Scope string `json:"scope"`
KnowledgeBaseExternalID string `json:"knowledge_base_external_id"`
Tags []string `json:"tags"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type TemplateListFilterArgs struct {
NameKeyword *string `json:"name_keyword"`
TargetContainer *TemplateContainer `json:"target_container"`
KnowledgeBaseID *string `json:"knowledge_base_id"`
Type *DocumentType `json:"type"`
}
type TemplateListByExternalRequest struct {
CreatorExternalID string `json:"creator_external_id"`
FilterArgs *TemplateListFilterArgs `json:"filter_args"`
SortArgs SortArgs `json:"sort_args"`
Pagination Pagination `json:"pagination"`
}
type CreateRecentTemplateRequest struct {
UserExternalID string `json:"user_external_id"`
TemplateID int64 `json:"template_id"`
AccessTime time.Time `json:"access_time"`
}
type ListRecentTemplatesRequest struct {
UserExternalID string `json:"user_external_id"`
StartTime *time.Time `json:"start_time,omitempty"`
EndTime *time.Time `json:"end_time,omitempty"`
Pagination Pagination `json:"pagination"`
}
+70
View File
@@ -0,0 +1,70 @@
package dto
import (
"time"
"github.com/XingfenD/yoresee_doc/internal/model"
)
type UserBase struct {
ExternalID string `json:"external_id"`
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
AvatarObjectKey string `json:"-"`
AvatarVersion int64 `json:"-"`
Status int `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type UserCreate struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
InvitationCode *string `json:"invitation_code"`
}
type UserUpdate struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Status int `json:"status"`
InvitationCode *string `json:"invitation_code"`
}
type UpdateProfileRequest struct {
Username *string `json:"username"`
Email *string `json:"email"`
Nickname *string `json:"nickname"`
Password *string `json:"password"`
AvatarFile []byte `json:"avatar_file"`
AvatarFilename *string `json:"avatar_filename"`
AvatarContentType *string `json:"avatar_content_type"`
}
type UserResponse struct {
UserBase
InvitationCode *string `json:"invitation_code"`
}
func NewUserResponseFromModel(user *model.User) *UserResponse {
return &UserResponse{
UserBase: UserBase{
ExternalID: user.ExternalID,
Username: user.Username,
Email: user.Email,
Nickname: user.Nickname,
Avatar: "",
AvatarObjectKey: user.AvatarObjectKey,
AvatarVersion: user.AvatarVersion,
Status: user.Status,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
},
InvitationCode: user.InvitationCode,
}
}
+56
View File
@@ -0,0 +1,56 @@
package dto
type UserGroupResponse struct {
ExternalID string `json:"external_id"`
Name string `json:"name"`
Description string `json:"description"`
CreatorUserExternalID string `json:"creator_user_external_id"`
MemberCount int `json:"member_count"`
}
type ListUserGroupsRequest struct {
Keyword *string `json:"keyword"`
Pagination Pagination `json:"pagination"`
}
type GetUserGroupRequest struct {
ExternalID string `json:"external_id"`
}
type CreateUserGroupRequest struct {
CreatorUserExternalID string `json:"creator_user_external_id"`
Name string `json:"name"`
Description string `json:"description"`
MemberUserExternalIDs []string `json:"member_user_external_ids"`
}
type UpdateUserGroupRequest struct {
ExternalID string `json:"external_id"`
Name *string `json:"name"`
Description *string `json:"description"`
SyncMembers bool `json:"sync_members"`
MemberUserExternalIDs []string `json:"member_user_external_ids"`
}
type DeleteUserGroupRequest struct {
ExternalID string `json:"external_id"`
}
type ListUsersRequest struct {
Keyword *string `json:"keyword"`
Pagination Pagination `json:"pagination"`
}
type UpdateUserRequest struct {
ExternalID string `json:"external_id"`
Username *string `json:"username"`
Email *string `json:"email"`
Nickname *string `json:"nickname"`
Status *int32 `json:"status"`
}
type ListUserGroupMembersRequest struct {
ExternalID string `json:"external_id"`
Keyword *string `json:"keyword"`
Pagination Pagination `json:"pagination"`
}
@@ -0,0 +1,31 @@
package doc_container_mapper
import (
"github.com/XingfenD/yoresee_doc/internal/dto"
"github.com/XingfenD/yoresee_doc/internal/model"
pb "github.com/XingfenD/yoresee_doc/pkg/gen/yoresee_doc/v1"
)
type knowledgeBaseAdapter struct{}
func (knowledgeBaseAdapter) Name() string {
return string(dto.ContainerType_KnowledgeBase)
}
func (knowledgeBaseAdapter) ProtoType() pb.CreateDocumentContainerType {
return pb.CreateDocumentContainerType_CREATE_DOCUMENT_CONTAINER_TYPE_KNOWLEDGE_BASE
}
func (knowledgeBaseAdapter) DTOType() dto.ContainerType {
return dto.ContainerType_KnowledgeBase
}
func (knowledgeBaseAdapter) ModelType() model.ContainerType {
return model.ContainerType_KnowledgeBase
}
func (knowledgeBaseAdapter) RequiresKnowledgeBaseID() bool {
return true
}
func init() { RegisterMapper(knowledgeBaseAdapter{}) }
@@ -0,0 +1,31 @@
package doc_container_mapper
import (
"github.com/XingfenD/yoresee_doc/internal/dto"
"github.com/XingfenD/yoresee_doc/internal/model"
pb "github.com/XingfenD/yoresee_doc/pkg/gen/yoresee_doc/v1"
)
type ownAdapter struct{}
func (ownAdapter) Name() string {
return string(dto.ContainerType_Own)
}
func (ownAdapter) ProtoType() pb.CreateDocumentContainerType {
return pb.CreateDocumentContainerType_CREATE_DOCUMENT_CONTAINER_TYPE_OWN
}
func (ownAdapter) DTOType() dto.ContainerType {
return dto.ContainerType_Own
}
func (ownAdapter) ModelType() model.ContainerType {
return model.ContainerType_Own
}
func (ownAdapter) RequiresKnowledgeBaseID() bool {
return false
}
func init() { RegisterMapper(ownAdapter{}) }
@@ -0,0 +1,168 @@
package doc_container_mapper
import (
"fmt"
"strings"
"github.com/XingfenD/yoresee_doc/internal/dto"
"github.com/XingfenD/yoresee_doc/internal/model"
pb "github.com/XingfenD/yoresee_doc/pkg/gen/yoresee_doc/v1"
)
const defaultContainerName = "own"
// Mapper defines a document container type mapping across transport(dto/proto) and model layers.
type Mapper interface {
Name() string
ProtoType() pb.CreateDocumentContainerType
DTOType() dto.ContainerType
ModelType() model.ContainerType
RequiresKnowledgeBaseID() bool
}
type registry struct {
byName map[string]Mapper
byProto map[pb.CreateDocumentContainerType]Mapper
byDTO map[dto.ContainerType]Mapper
byModel map[model.ContainerType]Mapper
}
var globalRegistry = &registry{
byName: make(map[string]Mapper),
byProto: make(map[pb.CreateDocumentContainerType]Mapper),
byDTO: make(map[dto.ContainerType]Mapper),
byModel: make(map[model.ContainerType]Mapper),
}
func RegisterMapper(mapper Mapper) {
if err := globalRegistry.register(mapper); err != nil {
panic(err)
}
}
func (r *registry) register(mapper Mapper) error {
if mapper == nil {
return fmt.Errorf("document container mapper is nil")
}
name := normalizeName(mapper.Name())
if name == "" {
return fmt.Errorf("document container mapper name is empty")
}
protoType := mapper.ProtoType()
dtoType := mapper.DTOType()
modelType := mapper.ModelType()
if protoType == pb.CreateDocumentContainerType_CREATE_DOCUMENT_CONTAINER_TYPE_UNSPECIFIED || dtoType == "" || modelType == "" {
return fmt.Errorf("document container mapper %q has invalid mapping", name)
}
if _, exists := r.byName[name]; exists {
return fmt.Errorf("document container mapper %q already registered", name)
}
if _, exists := r.byProto[protoType]; exists {
return fmt.Errorf("document container proto mapping %v already registered", protoType)
}
if _, exists := r.byDTO[dtoType]; exists {
return fmt.Errorf("document container dto mapping %q already registered", dtoType)
}
if _, exists := r.byModel[modelType]; exists {
return fmt.Errorf("document container model mapping %q already registered", modelType)
}
r.byName[name] = mapper
r.byProto[protoType] = mapper
r.byDTO[dtoType] = mapper
r.byModel[modelType] = mapper
return nil
}
func normalizeName(name string) string {
return strings.ToLower(strings.TrimSpace(name))
}
func (r *registry) findByName(name string) Mapper {
return r.byName[normalizeName(name)]
}
func (r *registry) findByProto(t pb.CreateDocumentContainerType) Mapper {
return r.byProto[t]
}
func (r *registry) findByDTO(t dto.ContainerType) Mapper {
if mapper, ok := r.byDTO[t]; ok {
return mapper
}
return r.byName[normalizeName(string(t))]
}
func (r *registry) findByModel(t model.ContainerType) Mapper {
if mapper, ok := r.byModel[t]; ok {
return mapper
}
return r.byName[normalizeName(string(t))]
}
func (r *registry) defaultMapper() Mapper {
if mapper := r.findByName(defaultContainerName); mapper != nil {
return mapper
}
for _, mapper := range r.byName {
return mapper
}
return nil
}
func DefaultDTOType() dto.ContainerType {
mapper := globalRegistry.defaultMapper()
if mapper == nil {
return dto.ContainerType(defaultContainerName)
}
return mapper.DTOType()
}
func IsSupportedDTOType(t dto.ContainerType) bool {
return globalRegistry.findByDTO(t) != nil
}
func IsSupportedProtoType(t pb.CreateDocumentContainerType) bool {
return globalRegistry.findByProto(t) != nil
}
func FromProtoType(t pb.CreateDocumentContainerType) (dto.ContainerType, bool) {
if mapper := globalRegistry.findByProto(t); mapper != nil {
return mapper.DTOType(), true
}
return "", false
}
func ToProtoType(t dto.ContainerType) pb.CreateDocumentContainerType {
if mapper := globalRegistry.findByDTO(t); mapper != nil {
return mapper.ProtoType()
}
return pb.CreateDocumentContainerType_CREATE_DOCUMENT_CONTAINER_TYPE_UNSPECIFIED
}
func ToModelType(t dto.ContainerType) model.ContainerType {
if mapper := globalRegistry.findByDTO(t); mapper != nil {
return mapper.ModelType()
}
return model.ContainerType(DefaultDTOType())
}
func FromModelType(t model.ContainerType) dto.ContainerType {
if mapper := globalRegistry.findByModel(t); mapper != nil {
return mapper.DTOType()
}
normalized := normalizeName(string(t))
if normalized != "" {
return dto.ContainerType(normalized)
}
return DefaultDTOType()
}
func RequiresKnowledgeBaseID(t dto.ContainerType) bool {
if mapper := globalRegistry.findByDTO(t); mapper != nil {
return mapper.RequiresKnowledgeBaseID()
}
return false
}
@@ -0,0 +1,27 @@
package doc_type_mapper
import (
"github.com/XingfenD/yoresee_doc/internal/dto"
"github.com/XingfenD/yoresee_doc/internal/model"
pb "github.com/XingfenD/yoresee_doc/pkg/gen/yoresee_doc/v1"
)
type markdownAdapter struct{}
func (markdownAdapter) Name() string {
return string(dto.DocumentType_Markdown)
}
func (markdownAdapter) ProtoType() pb.DocumentType {
return pb.DocumentType_DOCUMENT_TYPE_MARKDOWN
}
func (markdownAdapter) DTOType() dto.DocumentType {
return dto.DocumentType_Markdown
}
func (markdownAdapter) ModelType() model.DocumentType {
return model.DocumentType_Markdown
}
func init() { RegisterMapper(markdownAdapter{}) }
@@ -0,0 +1,27 @@
package doc_type_mapper
import (
"github.com/XingfenD/yoresee_doc/internal/dto"
"github.com/XingfenD/yoresee_doc/internal/model"
pb "github.com/XingfenD/yoresee_doc/pkg/gen/yoresee_doc/v1"
)
type markdownSlideAdapter struct{}
func (markdownSlideAdapter) Name() string {
return string(dto.DocumentType_MarkdownSlide)
}
func (markdownSlideAdapter) ProtoType() pb.DocumentType {
return pb.DocumentType_DOCUMENT_TYPE_MARKDOWN_SLIDE
}
func (markdownSlideAdapter) DTOType() dto.DocumentType {
return dto.DocumentType_MarkdownSlide
}
func (markdownSlideAdapter) ModelType() model.DocumentType {
return model.DocumentType_MarkdownSlide
}
func init() { RegisterMapper(markdownSlideAdapter{}) }
+167
View File
@@ -0,0 +1,167 @@
package doc_type_mapper
import (
"fmt"
"strings"
"github.com/XingfenD/yoresee_doc/internal/dto"
"github.com/XingfenD/yoresee_doc/internal/model"
pb "github.com/XingfenD/yoresee_doc/pkg/gen/yoresee_doc/v1"
)
const defaultTypeName = "markdown"
// Mapper defines a document type mapping across transport(dto/proto) and model layers.
type Mapper interface {
Name() string
ProtoType() pb.DocumentType
DTOType() dto.DocumentType
ModelType() model.DocumentType
}
type registry struct {
byName map[string]Mapper
byProto map[pb.DocumentType]Mapper
byDTO map[dto.DocumentType]Mapper
byModel map[model.DocumentType]Mapper
}
var globalRegistry = &registry{
byName: make(map[string]Mapper),
byProto: make(map[pb.DocumentType]Mapper),
byDTO: make(map[dto.DocumentType]Mapper),
byModel: make(map[model.DocumentType]Mapper),
}
// RegisterMapper registers a document type mapper.
// Implementing a new type only requires adding a new mapper and calling this in init().
func RegisterMapper(mapper Mapper) {
if err := globalRegistry.register(mapper); err != nil {
panic(err)
}
}
func (r *registry) register(mapper Mapper) error {
if mapper == nil {
return fmt.Errorf("document type mapper is nil")
}
name := normalizeName(mapper.Name())
if name == "" {
return fmt.Errorf("document type mapper name is empty")
}
dtoType := mapper.DTOType()
modelType := mapper.ModelType()
protoType := mapper.ProtoType()
if dtoType == "" || modelType == "" || protoType == pb.DocumentType_DOCUMENT_TYPE_UNSPECIFIED {
return fmt.Errorf("document type mapper %q has invalid mapping", name)
}
if _, exists := r.byName[name]; exists {
return fmt.Errorf("document type mapper %q already registered", name)
}
if _, exists := r.byProto[protoType]; exists {
return fmt.Errorf("document type proto mapping %v already registered", protoType)
}
if _, exists := r.byDTO[dtoType]; exists {
return fmt.Errorf("document type dto mapping %q already registered", dtoType)
}
if _, exists := r.byModel[modelType]; exists {
return fmt.Errorf("document type model mapping %q already registered", modelType)
}
r.byName[name] = mapper
r.byProto[protoType] = mapper
r.byDTO[dtoType] = mapper
r.byModel[modelType] = mapper
return nil
}
func (r *registry) findByName(name string) Mapper {
return r.byName[normalizeName(name)]
}
func (r *registry) findByProto(t pb.DocumentType) Mapper {
return r.byProto[t]
}
func (r *registry) findByDTO(t dto.DocumentType) Mapper {
if mapper, ok := r.byDTO[t]; ok {
return mapper
}
return r.byName[normalizeName(string(t))]
}
func (r *registry) findByModel(t model.DocumentType) Mapper {
if mapper, ok := r.byModel[t]; ok {
return mapper
}
return r.byName[normalizeName(string(t))]
}
func (r *registry) defaultMapper() Mapper {
if mapper := r.findByName(defaultTypeName); mapper != nil {
return mapper
}
for _, mapper := range r.byName {
return mapper
}
return nil
}
func normalizeName(name string) string {
normalized := strings.ToLower(strings.TrimSpace(name))
switch normalized {
case "slide":
return "markdown_slide"
case "yoreseerichtext", "yoresee-rich-text":
return "yoresee_rich_text"
default:
return normalized
}
}
func DefaultDTOType() dto.DocumentType {
mapper := globalRegistry.defaultMapper()
if mapper == nil {
return dto.DocumentType(defaultTypeName)
}
return mapper.DTOType()
}
func IsSupportedDTOType(t dto.DocumentType) bool {
return globalRegistry.findByDTO(t) != nil
}
func FromProtoType(t pb.DocumentType) dto.DocumentType {
if mapper := globalRegistry.findByProto(t); mapper != nil {
return mapper.DTOType()
}
return DefaultDTOType()
}
func ToProtoType(t dto.DocumentType) pb.DocumentType {
if mapper := globalRegistry.findByDTO(t); mapper != nil {
return mapper.ProtoType()
}
return pb.DocumentType_DOCUMENT_TYPE_UNSPECIFIED
}
func ToModelType(t dto.DocumentType) model.DocumentType {
if mapper := globalRegistry.findByDTO(t); mapper != nil {
return mapper.ModelType()
}
return model.DocumentType(DefaultDTOType())
}
func FromModelType(t model.DocumentType) dto.DocumentType {
if mapper := globalRegistry.findByModel(t); mapper != nil {
return mapper.DTOType()
}
normalized := normalizeName(string(t))
if normalized != "" {
return dto.DocumentType(normalized)
}
return DefaultDTOType()
}
+27
View File
@@ -0,0 +1,27 @@
package doc_type_mapper
import (
"github.com/XingfenD/yoresee_doc/internal/dto"
"github.com/XingfenD/yoresee_doc/internal/model"
pb "github.com/XingfenD/yoresee_doc/pkg/gen/yoresee_doc/v1"
)
type tableAdapter struct{}
func (tableAdapter) Name() string {
return string(dto.DocumentType_Table)
}
func (tableAdapter) ProtoType() pb.DocumentType {
return pb.DocumentType_DOCUMENT_TYPE_TABLE
}
func (tableAdapter) DTOType() dto.DocumentType {
return dto.DocumentType_Table
}
func (tableAdapter) ModelType() model.DocumentType {
return model.DocumentType_Table
}
func init() { RegisterMapper(tableAdapter{}) }
@@ -0,0 +1,27 @@
package doc_type_mapper
import (
"github.com/XingfenD/yoresee_doc/internal/dto"
"github.com/XingfenD/yoresee_doc/internal/model"
pb "github.com/XingfenD/yoresee_doc/pkg/gen/yoresee_doc/v1"
)
type yoreseeRichTextAdapter struct{}
func (yoreseeRichTextAdapter) Name() string {
return string(dto.DocumentType_YoreseeRichText)
}
func (yoreseeRichTextAdapter) ProtoType() pb.DocumentType {
return pb.DocumentType_DOCUMENT_TYPE_YORESEE_RICH_TEXT
}
func (yoreseeRichTextAdapter) DTOType() dto.DocumentType {
return dto.DocumentType_YoreseeRichText
}
func (yoreseeRichTextAdapter) ModelType() model.DocumentType {
return model.DocumentType_YoreseeRichText
}
func init() { RegisterMapper(yoreseeRichTextAdapter{}) }
@@ -0,0 +1,34 @@
package template_container_mapper
import (
"github.com/XingfenD/yoresee_doc/internal/dto"
pb "github.com/XingfenD/yoresee_doc/pkg/gen/yoresee_doc/v1"
)
type knowledgeBaseAdapter struct{}
func (knowledgeBaseAdapter) Name() string {
return "knowledge_base"
}
func (knowledgeBaseAdapter) ProtoType() pb.CreateTemplateContainer {
return pb.CreateTemplateContainer_KNOWLEDGEBASE_TEMPLATE
}
func (knowledgeBaseAdapter) DTOType() dto.TemplateContainer {
return dto.TemplateContainerKnowledgeBase
}
func (knowledgeBaseAdapter) Scope() string {
return "knowledge_base"
}
func (knowledgeBaseAdapter) IsPublic() bool {
return false
}
func (knowledgeBaseAdapter) RequiresKnowledgeBaseID() bool {
return true
}
func init() { RegisterMapper(knowledgeBaseAdapter{}) }
@@ -0,0 +1,34 @@
package template_container_mapper
import (
"github.com/XingfenD/yoresee_doc/internal/dto"
pb "github.com/XingfenD/yoresee_doc/pkg/gen/yoresee_doc/v1"
)
type ownAdapter struct{}
func (ownAdapter) Name() string {
return "own"
}
func (ownAdapter) ProtoType() pb.CreateTemplateContainer {
return pb.CreateTemplateContainer_OWN_TEMPLATE
}
func (ownAdapter) DTOType() dto.TemplateContainer {
return dto.TemplateContainerOwn
}
func (ownAdapter) Scope() string {
return "private"
}
func (ownAdapter) IsPublic() bool {
return false
}
func (ownAdapter) RequiresKnowledgeBaseID() bool {
return false
}
func init() { RegisterMapper(ownAdapter{}) }
@@ -0,0 +1,34 @@
package template_container_mapper
import (
"github.com/XingfenD/yoresee_doc/internal/dto"
pb "github.com/XingfenD/yoresee_doc/pkg/gen/yoresee_doc/v1"
)
type publicAdapter struct{}
func (publicAdapter) Name() string {
return "public"
}
func (publicAdapter) ProtoType() pb.CreateTemplateContainer {
return pb.CreateTemplateContainer_PUBLIC_TEMPLATE
}
func (publicAdapter) DTOType() dto.TemplateContainer {
return dto.TemplateContainerPublic
}
func (publicAdapter) Scope() string {
return "system"
}
func (publicAdapter) IsPublic() bool {
return true
}
func (publicAdapter) RequiresKnowledgeBaseID() bool {
return false
}
func init() { RegisterMapper(publicAdapter{}) }
@@ -0,0 +1,151 @@
package template_container_mapper
import (
"fmt"
"strings"
"github.com/XingfenD/yoresee_doc/internal/dto"
pb "github.com/XingfenD/yoresee_doc/pkg/gen/yoresee_doc/v1"
)
const defaultContainerName = "own"
type Mapper interface {
Name() string
ProtoType() pb.CreateTemplateContainer
DTOType() dto.TemplateContainer
Scope() string
IsPublic() bool
RequiresKnowledgeBaseID() bool
}
type registry struct {
byName map[string]Mapper
byProto map[pb.CreateTemplateContainer]Mapper
byDTO map[dto.TemplateContainer]Mapper
}
var globalRegistry = &registry{
byName: make(map[string]Mapper),
byProto: make(map[pb.CreateTemplateContainer]Mapper),
byDTO: make(map[dto.TemplateContainer]Mapper),
}
func RegisterMapper(mapper Mapper) {
if err := globalRegistry.register(mapper); err != nil {
panic(err)
}
}
func (r *registry) register(mapper Mapper) error {
if mapper == nil {
return fmt.Errorf("template container mapper is nil")
}
name := normalizeName(mapper.Name())
if name == "" {
return fmt.Errorf("template container mapper name is empty")
}
protoType := mapper.ProtoType()
dtoType := mapper.DTOType()
if dtoType == "" {
return fmt.Errorf("template container mapper %q has invalid dto mapping", name)
}
if strings.TrimSpace(mapper.Scope()) == "" {
return fmt.Errorf("template container mapper %q has empty scope mapping", name)
}
if _, exists := r.byName[name]; exists {
return fmt.Errorf("template container mapper %q already registered", name)
}
if _, exists := r.byProto[protoType]; exists {
return fmt.Errorf("template container proto mapping %v already registered", protoType)
}
if _, exists := r.byDTO[dtoType]; exists {
return fmt.Errorf("template container dto mapping %q already registered", dtoType)
}
r.byName[name] = mapper
r.byProto[protoType] = mapper
r.byDTO[dtoType] = mapper
return nil
}
func normalizeName(name string) string {
return strings.ToLower(strings.TrimSpace(name))
}
func (r *registry) findByName(name string) Mapper {
return r.byName[normalizeName(name)]
}
func (r *registry) findByProto(t pb.CreateTemplateContainer) Mapper {
return r.byProto[t]
}
func (r *registry) findByDTO(t dto.TemplateContainer) Mapper {
return r.byDTO[t]
}
func (r *registry) defaultMapper() Mapper {
if mapper := r.findByName(defaultContainerName); mapper != nil {
return mapper
}
for _, mapper := range r.byName {
return mapper
}
return nil
}
func DefaultDTOType() dto.TemplateContainer {
mapper := globalRegistry.defaultMapper()
if mapper == nil {
return dto.TemplateContainerOwn
}
return mapper.DTOType()
}
func IsSupportedDTOType(t dto.TemplateContainer) bool {
return globalRegistry.findByDTO(t) != nil
}
func FromProtoType(t pb.CreateTemplateContainer) dto.TemplateContainer {
if mapper := globalRegistry.findByProto(t); mapper != nil {
return mapper.DTOType()
}
return DefaultDTOType()
}
func ToProtoType(t dto.TemplateContainer) pb.CreateTemplateContainer {
if mapper := globalRegistry.findByDTO(t); mapper != nil {
return mapper.ProtoType()
}
return pb.CreateTemplateContainer_OWN_TEMPLATE
}
func ToScope(t dto.TemplateContainer) string {
if mapper := globalRegistry.findByDTO(t); mapper != nil {
return mapper.Scope()
}
if mapper := globalRegistry.defaultMapper(); mapper != nil {
return mapper.Scope()
}
return "private"
}
func ToIsPublic(t dto.TemplateContainer) bool {
if mapper := globalRegistry.findByDTO(t); mapper != nil {
return mapper.IsPublic()
}
if mapper := globalRegistry.defaultMapper(); mapper != nil {
return mapper.IsPublic()
}
return false
}
func RequiresKnowledgeBaseID(t dto.TemplateContainer) bool {
if mapper := globalRegistry.findByDTO(t); mapper != nil {
return mapper.RequiresKnowledgeBaseID()
}
return false
}
+39
View File
@@ -0,0 +1,39 @@
package media
import (
"strings"
)
const (
MaxAvatarSize = 5 * 1024 * 1024
defaultAvatarExtension = ".jpg"
)
var avatarContentTypes = map[string]string{
"image/jpeg": ".jpg",
"image/png": ".png",
"image/webp": ".webp",
}
func NormalizeAvatarContentType(file []byte, provided string) string {
return NormalizeContentTypeByAllowed(file, provided, avatarContentTypes)
}
func IsSupportedAvatarContentType(contentType string) bool {
return IsContentTypeAllowed(contentType, avatarContentTypes)
}
func ResolveAvatarExt(filename, contentType string) string {
return ResolveExtension(filename, contentType, avatarContentTypes, defaultAvatarExtension)
}
func BuildAvatarObjectKey(userExternalID string, avatarVersion int64, ext string) string {
return BuildVersionedObjectKey("avatars", userExternalID, avatarVersion, ext)
}
func BuildAvatarURL(userExternalID, objectKey string, avatarVersion int64) string {
if strings.TrimSpace(userExternalID) == "" || strings.TrimSpace(objectKey) == "" {
return ""
}
return BuildStorageURL(objectKey)
}
+84
View File
@@ -0,0 +1,84 @@
package media
import (
"fmt"
"mime"
"net/http"
"path/filepath"
"strings"
"github.com/XingfenD/yoresee_doc/pkg/storage"
)
func normalizeMIMEType(contentType string) string {
normalized := strings.ToLower(strings.TrimSpace(contentType))
if idx := strings.Index(normalized, ";"); idx > 0 {
normalized = strings.TrimSpace(normalized[:idx])
}
if normalized == "image/jpg" {
return "image/jpeg"
}
return normalized
}
func NormalizeContentTypeByAllowed(file []byte, provided string, allowed map[string]string) string {
normalized := normalizeMIMEType(provided)
if _, ok := allowed[normalized]; ok {
return normalized
}
detected := normalizeMIMEType(http.DetectContentType(file))
if _, ok := allowed[detected]; ok {
return detected
}
return ""
}
func IsContentTypeAllowed(contentType string, allowed map[string]string) bool {
_, ok := allowed[normalizeMIMEType(contentType)]
return ok
}
func ResolveExtension(filename, contentType string, allowed map[string]string, defaultExt string) string {
ext := strings.TrimSpace(strings.ToLower(filepath.Ext(filename)))
if ext != "" {
for _, candidate := range allowed {
if ext == candidate {
return ext
}
}
}
contentType = normalizeMIMEType(contentType)
if mapped, ok := allowed[contentType]; ok {
return mapped
}
exts, err := mime.ExtensionsByType(contentType)
if err == nil && len(exts) > 0 {
for _, candidate := range exts {
candidate = strings.TrimSpace(strings.ToLower(candidate))
for _, allowedExt := range allowed {
if candidate == allowedExt {
return candidate
}
}
}
}
return strings.TrimSpace(strings.ToLower(defaultExt))
}
func BuildVersionedObjectKey(prefix, ownerExternalID string, version int64, ext string) string {
prefix = strings.Trim(strings.TrimSpace(prefix), "/")
if prefix == "" {
return fmt.Sprintf("%s/v%d%s", ownerExternalID, version, ext)
}
return fmt.Sprintf("%s/%s/v%d%s", prefix, ownerExternalID, version, ext)
}
func BuildStorageURL(objectKey string) string {
if strings.TrimSpace(objectKey) == "" {
return ""
}
return storage.BuildPublicObjectPath(objectKey)
}
+41
View File
@@ -0,0 +1,41 @@
package middleware
import (
"strings"
"github.com/XingfenD/yoresee_doc/internal/auth"
"github.com/XingfenD/yoresee_doc/internal/status"
)
var JWTAuth = &JWTAuthMiddleware{}
type JWTAuthMiddleware struct {
}
func (m *JWTAuthMiddleware) ValidateAuthorizationHeader(authHeader string) (*auth.Claims, error) {
return m.handle(authHeader)
}
func (m *JWTAuthMiddleware) handle(authHeader string) (*auth.Claims, error) {
if authHeader == "" {
return nil, status.GenErrWithCustomMsg(status.StatusTokenInvalid, "unlogin or illegal access")
}
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
return nil, status.GenErrWithCustomMsg(status.StatusTokenInvalid, "invalid token format")
}
token := parts[1]
claims, err := auth.ParseToken(token)
if err != nil {
return nil, status.StatusTokenInvalid
}
if (&auth.JWTValidator{}).IsExpired(claims) {
return nil, status.StatusTokenExpired
}
if err := auth.ValidateJWTTokenInRedis(claims.ExternalID, token); err != nil {
return nil, err
}
return claims, nil
}
+26
View File
@@ -0,0 +1,26 @@
package model
import (
"time"
"gorm.io/gorm"
)
type Attachment struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
ExternalID string `gorm:"size:100;unique;not null" json:"external_id"`
DocumentID int64 `gorm:"not null;index" json:"document_id"`
Name string `gorm:"size:255;not null" json:"name"`
Path string `gorm:"size:512;not null" json:"path"`
Size int64 `json:"size"`
MimeType string `gorm:"size:100" json:"mime_type"`
UserID int64 `gorm:"not null" json:"user_id"`
PresignedURL string `gorm:"-" json:"presigned_url"` // 预签名URL,不存储到数据库
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (Attachment) TableName() string {
return "attachments"
}
+24
View File
@@ -0,0 +1,24 @@
package model
import (
"time"
"gorm.io/gorm"
)
type DocumentComment struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
ExternalID string `gorm:"size:100;unique;index;not null" json:"external_id"`
DocumentID int64 `gorm:"index;not null" json:"document_id"`
ParentID int64 `gorm:"default:0;index" json:"parent_id"`
CreatorID int64 `gorm:"index;not null" json:"creator_id"`
Content string `gorm:"type:text;not null" json:"content"`
AnchorID string `gorm:"size:128;index" json:"anchor_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (DocumentComment) TableName() string {
return "document_comments"
}
+45
View File
@@ -0,0 +1,45 @@
package model
import (
"time"
"gorm.io/gorm"
)
type DocumentType string
const DocumentType_Markdown DocumentType = "markdown"
const DocumentType_Table DocumentType = "table"
const DocumentType_MarkdownSlide DocumentType = "markdown_slide"
const DocumentType_YoreseeRichText DocumentType = "yoresee_rich_text"
type ContainerType string
const ContainerType_Own ContainerType = "own"
const ContainerType_KnowledgeBase ContainerType = "knowledge_base"
type Document struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
ExternalID string `gorm:"size:100;unique;index;not null" json:"external_id"`
Title string `gorm:"size:200;not null" json:"title"`
Type DocumentType `gorm:"size:20;default:'markdown'" json:"type"`
Summary string `gorm:"type:text" json:"summary"`
ParentID int64 `gorm:"default:0;index" json:"parent_id"` // 0 means root
UserID int64 `gorm:"not null;index" json:"user_id"`
KnowledgeID *int64 `gorm:"index" json:"knowledge_id"`
ContainerType ContainerType `gorm:"not null;default:'own';index" json:"containter_type"`
IsPublic bool `gorm:"default:false;index" json:"is_public"`
Tags []string `gorm:"serializer:json" json:"tags"`
Path string `gorm:"type:ltree;not null;default:'';index:idx_path_gist,using:gist"`
Depth int `gorm:"not null;default:0;index"`
ViewCount int `gorm:"default:0" json:"view_count"`
EditCount int `gorm:"default:0" json:"edit_count"`
Content string `gorm:"type:text" json:"content"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (Document) TableName() string {
return "document_metas"
}
+18
View File
@@ -0,0 +1,18 @@
package model
import "time"
type DocumentVersion struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
DocumentID int64 `gorm:"not null;index" json:"document_id"`
Version int `gorm:"not null" json:"version"`
Title string `gorm:"size:200;not null" json:"title"`
Content string `gorm:"type:text" json:"content"`
UserID int64 `gorm:"not null;index" json:"user_id"`
ChangeSummary string `gorm:"type:text" json:"change_summary"`
CreatedAt time.Time `json:"created_at"`
}
func (DocumentVersion) TableName() string {
return "document_versions"
}
+14
View File
@@ -0,0 +1,14 @@
package model
import "time"
type DocumentYjsSnapshot struct {
DocID int64 `gorm:"primaryKey;column:doc_id" json:"doc_id"`
YjsState []byte `gorm:"type:bytea;not null" json:"yjs_state"`
Version int64 `gorm:"not null" json:"version"`
UpdatedAt time.Time `json:"updated_at"`
}
func (DocumentYjsSnapshot) TableName() string {
return "documents_yjs_snapshot"
}
+22
View File
@@ -0,0 +1,22 @@
package model
import (
"time"
)
type Invitation struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Code string `gorm:"size:32;unique;not null" json:"code"`
CreatedBy int64 `gorm:"not null" json:"created_by"`
UsedCnt int64 `gorm:"default:0" json:"used_cnt"`
MaxUsedCnt *int64 `gorm:"default:1" json:"max_used_cnt"`
ExpiresAt *time.Time `json:"expires_at"`
Note *string `gorm:"type:text" json:"note"`
CreatedAt time.Time `json:"created_at"`
DeletedAt *time.Time `gorm:"index" json:"deleted_at"`
Disabled bool `gorm:"index,default:false" json:"disabled_at"`
}
func (Invitation) TableName() string {
return "invitations"
}
+17
View File
@@ -0,0 +1,17 @@
package model
import "time"
type InvitationRecord struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Code string `gorm:"size:32;index;not null" json:"code"`
UsedByUserID *int64 `gorm:"index" json:"used_by_user_id"`
UsedBy string `gorm:"size:100" json:"used_by"`
Status string `gorm:"size:20;index" json:"status"`
UsedAt time.Time `json:"used_at"`
CreatedAt time.Time `json:"created_at"`
}
func (InvitationRecord) TableName() string {
return "invitation_records"
}
+33
View File
@@ -0,0 +1,33 @@
package model
import (
"time"
)
type KnowledgeBase struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
ExternalID string `gorm:"size:100;unique;not null;index" json:"external_id"`
Name string `gorm:"size:100;not null" json:"name"`
Description string `gorm:"size:255" json:"description"`
Cover string `gorm:"size:255" json:"cover"`
CreatorUserID int64 `gorm:"not null" json:"creator_user_id"` // creator user id
IsPublic bool `gorm:"default:false" json:"is_public"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt time.Time `gorm:"index" json:"deleted_at"`
}
type RecentKnowledgeBase struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"not null;index" json:"user_id"`
KnowledgeBaseID int64 `gorm:"not null;index" json:"knowledge_base_id"`
AccessedAt time.Time `gorm:"not null" json:"accessed_at"`
}
func (RecentKnowledgeBase) TableName() string {
return "recent_knowledge_bases"
}
func (KnowledgeBase) TableName() string {
return "knowledge_bases"
}
+24
View File
@@ -0,0 +1,24 @@
package model
type MembershipType int64
const (
MembershipType_UserGroup MembershipType = 1 // equals to SubjectTypeUserGroup
MembershipType_OrgNode MembershipType = 2 // equals to SubjectTypeOrgNode
)
type MembershipRelation struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
Type MembershipType `gorm:"not null;index" json:"type"` // 1: UserGroup, 2: OrgNode
UserID int64 `gorm:"not null;index" json:"user_id"`
MembershipID int64 `gorm:"not null;index" json:"membership_id"` // user_group_id or org_node_id
// virtual field
User User `gorm:"foreignKey:UserID;references:ID"`
}
// query all user belong to membership with type and member_id
func (MembershipRelation) TableName() string {
return "membership"
}
+22
View File
@@ -0,0 +1,22 @@
package model
import (
"time"
)
type Notification struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
ExternalID string `gorm:"size:100;unique;index" json:"external_id"`
ReceiverID int64 `gorm:"index;not null" json:"receiver_id"`
Type string `gorm:"size:64;not null" json:"type"`
Status string `gorm:"size:32;not null;default:unread" json:"status"`
Title string `gorm:"size:255" json:"title"`
Content string `gorm:"size:1024" json:"content"`
Payload string `gorm:"type:jsonb" json:"payload"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Notification) TableName() string {
return "notifications"
}
+15
View File
@@ -0,0 +1,15 @@
package model
type OrgNodeMeta struct {
ID int64 `gorm:"primaryKey;autoIncrement"` // org_node_id
ExternalID string `gorm:"not null;index;unique" json:"external_id"` // external org_node_id
ParentID int64 `gorm:"not null;index" json:"parent_id"` // org_node_id, root node is 0
Name string `gorm:"not null;index" json:"name"`
Path string `gorm:"type:ltree;not null;index" json:"path"` // format: n<ID>.n<ID>...
Description string `gorm:"type:text;index" json:"description"`
CreatorID int64 `gorm:"not null;index" json:"creator_id"` // user_id
}
func (OrgNodeMeta) TableName() string {
return "org_node_meta"
}
+14
View File
@@ -0,0 +1,14 @@
package model
import "time"
type RecentDocument struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"not null;index" json:"user_id"`
DocumentID int64 `gorm:"not null;index" json:"document_id"`
AccessedAt time.Time `gorm:"not null" json:"accessed_at"`
}
func (RecentDocument) TableName() string {
return "recent_documents"
}
+14
View File
@@ -0,0 +1,14 @@
package model
import "time"
type RecentTemplate struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"not null;index" json:"user_id"`
TemplateID int64 `gorm:"not null;index" json:"template_id"`
AccessedAt time.Time `gorm:"not null" json:"accessed_at"`
}
func (RecentTemplate) TableName() string {
return "recent_templates"
}
+24
View File
@@ -0,0 +1,24 @@
package model
import (
"time"
)
type Template struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
Description string `gorm:"type:text" json:"description"`
DocumentType DocumentType `gorm:"column:document_type;size:20;default:'markdown'" json:"document_type"`
Content string `gorm:"type:text" json:"content"`
UserID int64 `gorm:"not null" json:"user_id"`
Scope string `gorm:"size:20;default:'private'" json:"scope"` // private, system, knowledge_base
KnowledgeBaseID *int64 `json:"knowledge_base_id"`
IsPublic bool `gorm:"default:false" json:"is_public"` // Deprecated, keep for migration
Tags []string `gorm:"serializer:json" json:"tags"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Template) TableName() string {
return "templates"
}
+29
View File
@@ -0,0 +1,29 @@
package model
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
ExternalID string `gorm:"size:100;unique" json:"external_id"`
Username string `gorm:"size:50;not null" json:"username"`
Email string `gorm:"size:100;unique;not null" json:"email"`
PasswordHash string `gorm:"size:255;not null" json:"-"`
Nickname string `gorm:"size:50" json:"nickname"`
Avatar string `gorm:"size:255" json:"avatar"` // deprecated
AvatarObjectKey string `gorm:"size:255" json:"avatar_object_key"`
AvatarVersion int64 `gorm:"not null;default:0" json:"avatar_version"`
AvatarUpdatedAt *time.Time `json:"avatar_updated_at"`
Status int `gorm:"default:1" json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
InvitationCode *string `gorm:"size:32" json:"invitation_code"`
}
func (User) TableName() string {
return "users"
}
+13
View File
@@ -0,0 +1,13 @@
package model
type UserGroupMeta struct {
ID int64 `gorm:"primaryKey;autoIncrement"` // user_group_id
ExternalID string `gorm:"not null;index;unique" json:"external_id"` // external user_group_id
Name string `gorm:"not null;index" json:"name"`
CreatorID int64 `gorm:"not null;index" json:"creator_id"` // user_id
Description string `gorm:"type:text;index" json:"description"`
}
func (UserGroupMeta) TableName() string {
return "user_group_meta"
}
@@ -0,0 +1,47 @@
package attachment_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type AttachmentRepository struct {
db *gorm.DB
}
func NewAttachmentRepository(db *gorm.DB) *AttachmentRepository {
return &AttachmentRepository{db: db}
}
func (r *AttachmentRepository) Create(attachment *model.Attachment) error {
return r.db.Create(attachment).Error
}
func (r *AttachmentRepository) ListByDocumentID(documentID int64) ([]*model.Attachment, error) {
attachments := make([]*model.Attachment, 0)
if err := r.db.
Where("document_id = ?", documentID).
Order("created_at desc").
Find(&attachments).Error; err != nil {
return nil, err
}
return attachments, nil
}
func (r *AttachmentRepository) GetByExternalIDAndDocumentID(externalID string, documentID int64) (*model.Attachment, error) {
attachment := &model.Attachment{}
if err := r.db.
Where("external_id = ? AND document_id = ?", externalID, documentID).
First(attachment).Error; err != nil {
return nil, err
}
return attachment, nil
}
func (r *AttachmentRepository) DeleteByID(id int64) error {
return r.db.Delete(&model.Attachment{}, id).Error
}
func IsNotFound(err error) bool {
return err == gorm.ErrRecordNotFound
}
@@ -0,0 +1,11 @@
package comment_repo
import "gorm.io/gorm"
type CommentRepository struct {
db *gorm.DB
}
func NewCommentRepository(db *gorm.DB) *CommentRepository {
return &CommentRepository{db: db}
}
@@ -0,0 +1,35 @@
package comment_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type CommentCreateOperation struct {
repo *CommentRepository
item *model.DocumentComment
tx *gorm.DB
}
func (r *CommentRepository) Create(item *model.DocumentComment) *CommentCreateOperation {
return &CommentCreateOperation{
repo: r,
item: item,
}
}
func (op *CommentCreateOperation) WithTx(tx *gorm.DB) *CommentCreateOperation {
op.tx = tx
return op
}
func (op *CommentCreateOperation) Exec() error {
if op.item == nil {
return nil
}
db := op.repo.db
if op.tx != nil {
db = op.tx
}
return db.Create(op.item).Error
}
@@ -0,0 +1,32 @@
package comment_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type CommentDeleteOperation struct {
repo *CommentRepository
id int64
tx *gorm.DB
}
func (r *CommentRepository) Delete(id int64) *CommentDeleteOperation {
return &CommentDeleteOperation{
repo: r,
id: id,
}
}
func (op *CommentDeleteOperation) WithTx(tx *gorm.DB) *CommentDeleteOperation {
op.tx = tx
return op
}
func (op *CommentDeleteOperation) Exec() error {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
return db.Delete(&model.DocumentComment{}, op.id).Error
}
@@ -0,0 +1,36 @@
package comment_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type CommentGetByExternalIDOperation struct {
repo *CommentRepository
externalID string
tx *gorm.DB
}
func (r *CommentRepository) GetByExternalID(externalID string) *CommentGetByExternalIDOperation {
return &CommentGetByExternalIDOperation{
repo: r,
externalID: externalID,
}
}
func (op *CommentGetByExternalIDOperation) WithTx(tx *gorm.DB) *CommentGetByExternalIDOperation {
op.tx = tx
return op
}
func (op *CommentGetByExternalIDOperation) Exec() (*model.DocumentComment, error) {
var item model.DocumentComment
db := op.repo.db
if op.tx != nil {
db = op.tx
}
if err := db.First(&item, "external_id = ?", op.externalID).Error; err != nil {
return nil, err
}
return &item, nil
}
@@ -0,0 +1,66 @@
package comment_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type CommentListByDocumentOperation struct {
repo *CommentRepository
documentID int64
page int
pageSize int
tx *gorm.DB
}
func (r *CommentRepository) ListByDocument(documentID int64) *CommentListByDocumentOperation {
return &CommentListByDocumentOperation{
repo: r,
documentID: documentID,
}
}
func (op *CommentListByDocumentOperation) WithTx(tx *gorm.DB) *CommentListByDocumentOperation {
op.tx = tx
return op
}
func (op *CommentListByDocumentOperation) WithPagination(page, pageSize int) *CommentListByDocumentOperation {
op.page = page
op.pageSize = pageSize
return op
}
func (op *CommentListByDocumentOperation) ExecWithTotal() ([]model.DocumentComment, int64, error) {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
query := db.Model(&model.DocumentComment{}).
Where("document_id = ?", op.documentID).
Where("anchor_id IS NOT NULL AND anchor_id <> ''")
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
page := op.page
pageSize := op.pageSize
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
offset := (page - 1) * pageSize
var items []model.DocumentComment
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&items).Error; err != nil {
return nil, 0, err
}
return items, total, nil
}
@@ -0,0 +1,34 @@
package comment_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type CommentUpdateContentOperation struct {
repo *CommentRepository
id int64
content string
tx *gorm.DB
}
func (r *CommentRepository) UpdateContentByID(id int64, content string) *CommentUpdateContentOperation {
return &CommentUpdateContentOperation{
repo: r,
id: id,
content: content,
}
}
func (op *CommentUpdateContentOperation) WithTx(tx *gorm.DB) *CommentUpdateContentOperation {
op.tx = tx
return op
}
func (op *CommentUpdateContentOperation) Exec() error {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
return db.Model(&model.DocumentComment{}).Where("id = ?", op.id).Update("content", op.content).Error
}
@@ -0,0 +1,32 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentCreateOperation struct {
repo *DocumentRepository
doc *model.Document
tx *gorm.DB
}
func (r *DocumentRepository) Create(doc *model.Document) *DocumentCreateOperation {
return &DocumentCreateOperation{
repo: r,
doc: doc,
}
}
func (op *DocumentCreateOperation) WithTx(tx *gorm.DB) *DocumentCreateOperation {
op.tx = tx
return op
}
func (op *DocumentCreateOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
return op.tx.Create(op.doc).Error
}
@@ -0,0 +1,36 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type UpsertRecentDocumentOperation struct {
repo *DocumentRepository
m *model.RecentDocument
tx *gorm.DB
}
func (r *DocumentRepository) UpsertRecentDocument(m *model.RecentDocument) *UpsertRecentDocumentOperation {
return &UpsertRecentDocumentOperation{
repo: r,
m: m,
}
}
func (op *UpsertRecentDocumentOperation) WithTx(tx *gorm.DB) *UpsertRecentDocumentOperation {
op.tx = tx
return op
}
func (op *UpsertRecentDocumentOperation) Exec() error {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
return db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "document_id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"accessed_at": op.m.AccessedAt}),
}).Create(op.m).Error
}
@@ -0,0 +1,92 @@
package document_repo
import (
"context"
cache_loader "github.com/XingfenD/yoresee_doc/internal/cache"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/pkg/key"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type DocumentRepository struct {
db *gorm.DB
redis *redis.Client
Loader cache_loader.Loader
}
func NewDocumentRepository(db *gorm.DB, redis *redis.Client) *DocumentRepository {
return &DocumentRepository{
db: db,
redis: redis,
Loader: *cache_loader.NewLoader(redis),
}
}
type DocumentDeleteOperation struct {
repo *DocumentRepository
id int64
tx *gorm.DB
ctx context.Context
}
func (r *DocumentRepository) Delete(id int64) *DocumentDeleteOperation {
return &DocumentDeleteOperation{
repo: r,
id: id,
ctx: context.Background(),
}
}
func (op *DocumentDeleteOperation) WithTx(tx *gorm.DB) *DocumentDeleteOperation {
op.tx = tx
return op
}
func (op *DocumentDeleteOperation) WithContext(ctx context.Context) *DocumentDeleteOperation {
op.ctx = ctx
return op
}
func (op *DocumentDeleteOperation) Exec() error {
var doc model.Document
var db *gorm.DB
var err error
if op.tx != nil {
db = op.tx
} else {
db = op.repo.db
}
if err := db.First(&doc, op.id).Error; err != nil {
return err
}
if op.tx == nil {
db = db.Begin()
defer func() {
if err != nil {
db.Rollback()
} else {
db.Commit()
}
}()
}
if err := db.Delete(&model.Document{}, op.id).Error; err != nil {
return err
}
if op.tx == nil {
docCacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, doc.ExternalID)
if err := op.repo.redis.Del(op.ctx, docCacheKey).Err(); err != nil {
}
if err := op.repo.BumpSubtreeVersionsByPath(op.ctx, doc.Path); err != nil {
}
}
return nil
}
@@ -0,0 +1,60 @@
package document_repo
import (
"context"
cache_loader "github.com/XingfenD/yoresee_doc/internal/cache"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/pkg/key"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type DocumentGetByExternalIDOperation struct {
repo *DocumentRepository
externalID string
tx *gorm.DB
}
func (r *DocumentRepository) GetByExternalID(externalID string) *DocumentGetByExternalIDOperation {
return &DocumentGetByExternalIDOperation{
repo: r,
externalID: externalID,
}
}
func (op *DocumentGetByExternalIDOperation) WithTx(tx *gorm.DB) *DocumentGetByExternalIDOperation {
op.tx = tx
return op
}
func (op *DocumentGetByExternalIDOperation) query(db *gorm.DB) (*model.Document, error) {
var document model.Document
var err error
err = db.First(&document, "external_id = ?", op.externalID).Error
return &document, err
}
func (op *DocumentGetByExternalIDOperation) Exec(ctx context.Context) (*model.Document, error) {
var err error
if op.tx != nil {
return op.query(op.tx)
}
documentCacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, op.externalID)
document, err := cache_loader.NewCacheLoadOperation[model.Document](&op.repo.Loader).
WithDBLoader(func() (*model.Document, error) {
return op.query(op.repo.db)
}).WithDefaultKeyAndParser(documentCacheKey, nil).
Exec(ctx)
if err != nil {
logrus.Errorf("load data failed for DocumentGetByExternalIDOperation: %+v", err)
return nil, err
}
return document, nil
}
@@ -0,0 +1,41 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentGetContentOperation struct {
repo *DocumentRepository
documentID int64
tx *gorm.DB
}
func (r *DocumentRepository) GetContent(documentID int64) *DocumentGetContentOperation {
return &DocumentGetContentOperation{
repo: r,
documentID: documentID,
}
}
func (op *DocumentGetContentOperation) WithTx(tx *gorm.DB) *DocumentGetContentOperation {
op.tx = tx
return op
}
func (op *DocumentGetContentOperation) Exec() (string, error) {
var docMeta model.Document
var err error
db := op.repo.db
if op.tx != nil {
db = op.tx
}
err = db.Where("id = ?", op.documentID).First(&docMeta).Error
if err != nil {
return "", err
}
return docMeta.Content, nil
}
@@ -0,0 +1,66 @@
package document_repo
import (
"context"
cache_loader "github.com/XingfenD/yoresee_doc/internal/cache"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/pkg/key"
"gorm.io/gorm"
)
type DocumentGetIDByExternalIDOperation struct {
repo *DocumentRepository
externalID string
tx *gorm.DB
}
func (r *DocumentRepository) GetIDByExternalID(externalID string) *DocumentGetIDByExternalIDOperation {
return &DocumentGetIDByExternalIDOperation{
repo: r,
externalID: externalID,
}
}
func (op *DocumentGetIDByExternalIDOperation) WithTx(tx *gorm.DB) *DocumentGetIDByExternalIDOperation {
op.tx = tx
return op
}
func (op DocumentGetIDByExternalIDOperation) query(tx *gorm.DB) (int64, error) {
var id int64
err := tx.Model(&model.Document{}).Where("external_id = ?", op.externalID).Pluck("id", &id).Error
if err != nil {
return 0, err
}
return id, nil
}
func (op *DocumentGetIDByExternalIDOperation) Exec(ctx context.Context) (int64, error) {
if op.tx != nil {
return op.query(op.tx)
}
extidKey := key.KeyIDByExternalID(key.KeyObjectTypeEnum_Doc, op.externalID)
modelKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, op.externalID)
id, err := cache_loader.NewCacheLoadOperation[int64](&op.repo.Loader).
WithDefaultKeyAndParser(extidKey, cache_loader.ParseInt64).
WithKeyAndParser(modelKey, cache_loader.ParseIDFromDocument).
WithDBLoader(func() (*int64, error) {
id, err := op.query(op.repo.db)
if err != nil {
return nil, err
}
return &id, nil
}).Exec(ctx)
if err != nil {
return 0, err
}
if id == nil {
return 0, nil
}
return *id, nil
}

Some files were not shown because too many files have changed in this diff Show More