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
+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
}
@@ -0,0 +1,149 @@
package document_repo
import (
"context"
"errors"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/pkg/key"
"gorm.io/gorm"
)
type DocumentGetSubtreeOperation struct {
repo *DocumentRepository
rootParentID int64
knowledgeID *int64
depth *int
directoryOnly bool
tx *gorm.DB
}
func (r *DocumentRepository) GetSubtree(rootParentID int64) *DocumentGetSubtreeOperation {
return &DocumentGetSubtreeOperation{
repo: r,
rootParentID: rootParentID,
}
}
func (op *DocumentGetSubtreeOperation) WithTx(tx *gorm.DB) *DocumentGetSubtreeOperation {
op.tx = tx
return op
}
func (op *DocumentGetSubtreeOperation) WithKnowledgeID(knowledgeID *int64) *DocumentGetSubtreeOperation {
op.knowledgeID = knowledgeID
return op
}
func (op *DocumentGetSubtreeOperation) WithDepth(depth *int) *DocumentGetSubtreeOperation {
op.depth = depth
return op
}
func (op *DocumentGetSubtreeOperation) WithDirectoryOnly(with bool) *DocumentGetSubtreeOperation {
op.directoryOnly = with
return op
}
func (op *DocumentGetSubtreeOperation) Exec(ctx context.Context) ([]*model.Document, error) {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
var documents []*model.Document
if op.depth != nil && *op.depth == 0 {
return documents, nil
}
type pathDepth struct {
Path string
Depth int
}
var root pathDepth
err := db.Model(&model.Document{}).
Select("path, depth").
Where("id = ?", op.rootParentID).
Take(&root).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return documents, nil
}
return nil, err
}
// in-transaction query should bypass cache to avoid stale reads
if op.tx != nil || op.repo.redis == nil {
return op.queryWithRoot(db, root.Path, root.Depth)
}
version, err := op.repo.getSubtreeVersion(ctx, root.Path)
if err == nil {
cacheKey := key.KeyDocSubtree(root.Path, version, op.depth)
if cachedIDs, ok, err := getCachedSubtreeIDs(ctx, cacheKey); err == nil && ok {
return op.repo.fetchDocumentsByIDs(cachedIDs)
}
val, err, _ := subtreeCacheSF.Do(cacheKey, func() (interface{}, error) {
dbDocs, err := op.queryWithRoot(db, root.Path, root.Depth)
if err != nil {
return nil, err
}
ids := make([]int64, 0, len(dbDocs))
for _, doc := range dbDocs {
ids = append(ids, doc.ID)
}
setCachedSubtreeIDs(ctx, cacheKey, ids)
return dbDocs, nil
})
if err != nil {
return nil, err
}
if typed, ok := val.([]*model.Document); ok {
return typed, nil
}
}
return op.queryWithRoot(db, root.Path, root.Depth)
}
func (op *DocumentGetSubtreeOperation) queryWithRoot(db *gorm.DB, rootPath string, rootDepth int) ([]*model.Document, error) {
var documents []*model.Document
query := `
SELECT *
FROM document_metas
WHERE deleted_at IS NULL
AND id <> ?
AND path <@ ?
`
if op.directoryOnly {
query = `
SELECT id, external_id, title, parent_id, type
FROM document_metas
WHERE deleted_at IS NULL
AND id <> ?
AND path <@ ?
`
}
args := []interface{}{op.rootParentID, rootPath}
if op.knowledgeID != nil {
query += " AND knowledge_id = ?"
args = append(args, *op.knowledgeID)
}
if op.depth != nil {
maxDepth := rootDepth + *op.depth
query += " AND depth <= ?"
args = append(args, maxDepth)
}
query += " ORDER BY depth, created_at"
if err := db.Raw(query, args...).Find(&documents).Error; err != nil {
return nil, err
}
return documents, nil
}
@@ -0,0 +1,64 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentGetSubtreeByKnowledgeIDOperation struct {
repo *DocumentRepository
knowledgeID int64
depth *int
tx *gorm.DB
}
func (r *DocumentRepository) GetSubtreeByKnowledgeID(knowledgeID int64) *DocumentGetSubtreeByKnowledgeIDOperation {
return &DocumentGetSubtreeByKnowledgeIDOperation{
repo: r,
knowledgeID: knowledgeID,
}
}
func (op *DocumentGetSubtreeByKnowledgeIDOperation) WithTx(tx *gorm.DB) *DocumentGetSubtreeByKnowledgeIDOperation {
op.tx = tx
return op
}
func (op *DocumentGetSubtreeByKnowledgeIDOperation) WithDepth(depth int) *DocumentGetSubtreeByKnowledgeIDOperation {
op.depth = &depth
return op
}
func (op *DocumentGetSubtreeByKnowledgeIDOperation) Exec() ([]model.Document, error) {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
var documents []model.Document
if op.depth != nil && *op.depth == 0 {
return documents, nil
}
query := `
SELECT *
FROM document_metas
WHERE deleted_at IS NULL
AND knowledge_id = ?
`
args := []interface{}{op.knowledgeID}
if op.depth != nil {
query += " AND depth <= ?"
args = append(args, *op.depth)
}
query += " ORDER BY depth, created_at"
err := db.Raw(query, args...).Find(&documents).Error
if err != nil {
return nil, err
}
return documents, nil
}
+246
View File
@@ -0,0 +1,246 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentsListOperation struct {
repo *DocumentRepository
model *model.Document
userID *int64
parentID *int64
knowledgeID *int64
listOwnDoc bool
ids []int64
titleKeyword *string
docType *string
tags []string
createTimeRangeStart *string
createTimeRangeEnd *string
updateTimeRangeStart *string
updateTimeRangeEnd *string
sortField string
sortDesc bool
page int
pageSize int
directoryOnly bool
tx *gorm.DB
}
func (r *DocumentRepository) ListDocuments(documentModel *model.Document) *DocumentsListOperation {
return &DocumentsListOperation{
repo: r,
model: documentModel,
}
}
func (op *DocumentsListOperation) WithUserID(userID *int64) *DocumentsListOperation {
op.userID = userID
return op
}
func (op *DocumentsListOperation) WithParentID(parentID *int64) *DocumentsListOperation {
op.parentID = parentID
return op
}
func (op *DocumentsListOperation) WithKnowledgeID(knowledgeID *int64) *DocumentsListOperation {
op.knowledgeID = knowledgeID
return op
}
func (op *DocumentsListOperation) WithListOwnDoc(listOwnDoc bool) *DocumentsListOperation {
op.listOwnDoc = listOwnDoc
return op
}
func (op *DocumentsListOperation) WithIDs(ids []int64) *DocumentsListOperation {
op.ids = ids
return op
}
func (op *DocumentsListOperation) WithTitleKeyword(titleKeyword *string) *DocumentsListOperation {
op.titleKeyword = titleKeyword
return op
}
func (op *DocumentsListOperation) WithType(docType *string) *DocumentsListOperation {
op.docType = docType
return op
}
func (op *DocumentsListOperation) WithTags(tags []string) *DocumentsListOperation {
op.tags = tags
return op
}
func (op *DocumentsListOperation) WithCreateTimeRange(start, end *string) *DocumentsListOperation {
op.createTimeRangeStart = start
op.createTimeRangeEnd = end
return op
}
func (op *DocumentsListOperation) WithUpdateTimeRange(start, end *string) *DocumentsListOperation {
op.updateTimeRangeStart = start
op.updateTimeRangeEnd = end
return op
}
func (op *DocumentsListOperation) WithPagination(page, pageSize int) *DocumentsListOperation {
op.page = page
op.pageSize = pageSize
return op
}
func (op *DocumentsListOperation) WithSort(field string, desc bool) *DocumentsListOperation {
op.sortField = field
op.sortDesc = desc
return op
}
func (op *DocumentsListOperation) WithDirectoryOnly(with bool) *DocumentsListOperation {
op.directoryOnly = with
return op
}
func (op *DocumentsListOperation) WithTx(tx *gorm.DB) *DocumentsListOperation {
op.tx = tx
return op
}
func (op *DocumentsListOperation) buildBaseQuery() *gorm.DB {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
dbQuery := db.Model(&model.Document{})
if op.directoryOnly {
dbQuery = dbQuery.Select("id, external_id, title, parent_id, type")
}
if op.model != nil {
dbQuery = dbQuery.Where(op.model)
}
if op.userID != nil {
dbQuery = dbQuery.Where("user_id = ?", *op.userID)
}
if op.parentID != nil {
dbQuery = dbQuery.Where("parent_id = ?", *op.parentID)
}
if op.listOwnDoc {
dbQuery = dbQuery.Where("knowledge_id IS NULL")
}
if op.ids != nil {
if len(op.ids) == 0 {
dbQuery = dbQuery.Where("1 = 0")
} else {
dbQuery = dbQuery.Where("id IN ?", op.ids)
}
}
if op.knowledgeID != nil {
dbQuery = dbQuery.Where("knowledge_id = ?", *op.knowledgeID)
}
if op.titleKeyword != nil && *op.titleKeyword != "" {
like := "%" + *op.titleKeyword + "%"
dbQuery = dbQuery.Where("(title LIKE ? OR content LIKE ?)", like, like)
}
if op.docType != nil && *op.docType != "" {
dbQuery = dbQuery.Where("type = ?", *op.docType)
}
if len(op.tags) > 0 {
for _, tag := range op.tags {
dbQuery = dbQuery.Where("JSON_CONTAINS(tags, ?)", "\""+tag+"\"")
}
}
if op.createTimeRangeStart != nil && *op.createTimeRangeStart != "" {
dbQuery = dbQuery.Where("created_at >= ?", *op.createTimeRangeStart)
}
if op.createTimeRangeEnd != nil && *op.createTimeRangeEnd != "" {
dbQuery = dbQuery.Where("created_at <= ?", *op.createTimeRangeEnd)
}
if op.updateTimeRangeStart != nil && *op.updateTimeRangeStart != "" {
dbQuery = dbQuery.Where("updated_at >= ?", *op.updateTimeRangeStart)
}
if op.updateTimeRangeEnd != nil && *op.updateTimeRangeEnd != "" {
dbQuery = dbQuery.Where("updated_at <= ?", *op.updateTimeRangeEnd)
}
return dbQuery
}
func (op *DocumentsListOperation) appendOtherArgs(db *gorm.DB) *gorm.DB {
dbQuery := db
sortField := op.sortField
if sortField == "" {
sortField = "created_at"
}
orderDirection := "ASC"
if op.sortDesc {
orderDirection = "DESC"
}
dbQuery = dbQuery.Order(sortField + " " + orderDirection)
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
dbQuery = dbQuery.Offset(offset).Limit(pageSize)
return dbQuery
}
func (op *DocumentsListOperation) Exec() ([]model.Document, error) {
dbQuery := op.buildBaseQuery()
dbQuery = op.appendOtherArgs(dbQuery)
var documents []model.Document
err := dbQuery.Find(&documents).Error
if err != nil {
return nil, err
}
return documents, nil
}
func (op *DocumentsListOperation) ExecWithTotal() ([]model.Document, int64, error) {
dbQuery := op.buildBaseQuery()
var total int64
err := dbQuery.Count(&total).Error
if err != nil {
return nil, 0, err
}
dbQuery = op.appendOtherArgs(dbQuery)
var documents []model.Document
err = dbQuery.Find(&documents).Error
if err != nil {
return nil, 0, err
}
return documents, total, nil
}
@@ -0,0 +1,82 @@
package document_repo
import (
"time"
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type ListRecentDocumentsOperation struct {
repo *DocumentRepository
userID int64
startTime *time.Time
endTime *time.Time
page int
pageSize int
tx *gorm.DB
}
func (r *DocumentRepository) ListRecentDocuments(userID int64) *ListRecentDocumentsOperation {
return &ListRecentDocumentsOperation{
repo: r,
userID: userID,
}
}
func (op *ListRecentDocumentsOperation) WithTimeRange(start, end *time.Time) *ListRecentDocumentsOperation {
op.startTime = start
op.endTime = end
return op
}
func (op *ListRecentDocumentsOperation) WithPagination(page, pageSize int) *ListRecentDocumentsOperation {
op.page = page
op.pageSize = pageSize
return op
}
func (op *ListRecentDocumentsOperation) WithTx(tx *gorm.DB) *ListRecentDocumentsOperation {
op.tx = tx
return op
}
func (op *ListRecentDocumentsOperation) Exec() ([]*model.RecentDocument, int64, error) {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
query := db.Model(&model.RecentDocument{}).
Where("user_id = ?", op.userID)
if op.startTime != nil {
query = query.Where("accessed_at >= ?", *op.startTime)
}
if op.endTime != nil {
query = query.Where("accessed_at <= ?", *op.endTime)
}
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 total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var records []*model.RecentDocument
if err := query.Order("accessed_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
return nil, 0, err
}
return records, total, nil
}
@@ -0,0 +1,29 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
)
func (r *DocumentRepository) MGetByIDs(ids []int64) ([]*model.Document, error) {
if len(ids) == 0 {
return []*model.Document{}, nil
}
var docs []*model.Document
if err := r.db.Model(&model.Document{}).Where("id IN ?", ids).Find(&docs).Error; err != nil {
return nil, err
}
docMap := make(map[int64]*model.Document, len(docs))
for _, doc := range docs {
docMap[doc.ID] = doc
}
ordered := make([]*model.Document, 0, len(ids))
for _, id := range ids {
if doc, ok := docMap[id]; ok {
ordered = append(ordered, doc)
}
}
return ordered, nil
}
@@ -0,0 +1,78 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentMoveSubtreeOperation struct {
repo *DocumentRepository
docID int64
newParentID int64
tx *gorm.DB
}
func (r *DocumentRepository) MoveSubtree(docID, newParentID int64) *DocumentMoveSubtreeOperation {
return &DocumentMoveSubtreeOperation{
repo: r,
docID: docID,
newParentID: newParentID,
}
}
func (op *DocumentMoveSubtreeOperation) WithTx(tx *gorm.DB) *DocumentMoveSubtreeOperation {
op.tx = tx
return op
}
func (op *DocumentMoveSubtreeOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
type pathDepth struct {
Path string
Depth int
}
var old pathDepth
if err := op.tx.Model(&model.Document{}).
Select("path, depth").
Where("id = ? AND deleted_at IS NULL", op.docID).
Take(&old).Error; err != nil {
return err
}
var newPath string
var newDepth int
if op.newParentID == 0 {
newDepth = 0
} else {
var parent pathDepth
if err := op.tx.Model(&model.Document{}).
Select("path, depth").
Where("id = ? AND deleted_at IS NULL", op.newParentID).
Take(&parent).Error; err != nil {
return err
}
newPath = parent.Path
newDepth = parent.Depth + 1
}
depthDelta := newDepth - old.Depth
if op.newParentID == 0 {
return op.tx.Exec(`
UPDATE document_metas
SET path = (?::text)::ltree || COALESCE(subpath(path, nlevel(?::ltree)), ''::ltree),
depth = depth + ?
WHERE path <@ ?::ltree AND deleted_at IS NULL
`, op.docID, old.Path, depthDelta, old.Path).Error
}
return op.tx.Exec(`
UPDATE document_metas
SET path = (?::ltree) || (?::text)::ltree || COALESCE(subpath(path, nlevel(?::ltree)), ''::ltree),
depth = depth + ?
WHERE path <@ ?::ltree AND deleted_at IS NULL
`, newPath, op.docID, old.Path, depthDelta, old.Path).Error
}
@@ -0,0 +1,137 @@
package document_repo
import (
"context"
"strconv"
"strings"
"time"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/pkg/cache"
"github.com/XingfenD/yoresee_doc/pkg/key"
"github.com/redis/go-redis/v9"
"github.com/sirupsen/logrus"
"golang.org/x/sync/singleflight"
"gorm.io/gorm"
)
const (
subtreeCacheTTL = 5 * time.Minute
subtreeEmptyTTL = 1 * time.Minute
subtreeCacheMaxDepth = 3
)
var subtreeCacheSF singleflight.Group
func splitPathPrefixes(path string) []string {
parts := strings.Split(path, ".")
out := make([]string, 0, len(parts))
for i := range parts {
out = append(out, strings.Join(parts[:i+1], "."))
}
return out
}
func getDocPathByID(tx *gorm.DB, docID int64) (string, error) {
type docPath struct {
Path string
}
var result docPath
err := tx.Model(&model.Document{}).
Select("path").
Where("id = ?", docID).
Take(&result).Error
if err != nil {
return "", err
}
return result.Path, nil
}
func (r *DocumentRepository) GetPathByID(id int64) (string, error) {
return getDocPathByID(r.db, id)
}
func (r *DocumentRepository) GetPathByIDWithTx(tx *gorm.DB, id int64) (string, error) {
return getDocPathByID(tx, id)
}
func (r *DocumentRepository) getSubtreeVersion(ctx context.Context, path string) (int64, error) {
if r.redis == nil {
return 0, nil
}
val, err := r.redis.Get(ctx, key.KeyDocSubtreeVersion(path)).Result()
if err == redis.Nil {
return 0, nil
}
if err != nil {
return 0, err
}
version, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return 0, nil
}
return version, nil
}
func (r *DocumentRepository) BumpSubtreeVersionsByPath(ctx context.Context, path string) error {
if r.redis == nil {
return nil
}
prefixes := splitPathPrefixes(path)
pipe := r.redis.Pipeline()
for _, prefix := range prefixes {
pipe.Incr(ctx, key.KeyDocSubtreeVersion(prefix))
}
_, err := pipe.Exec(ctx)
if err == redis.Nil {
return nil
}
return err
}
func getCachedSubtreeIDs(ctx context.Context, key string) ([]int64, bool, error) {
var ids []int64
ok, err := cache.GetJSON(ctx, key, &ids)
if err != nil {
return nil, false, err
}
if !ok {
return nil, false, nil
}
return ids, true, nil
}
func setCachedSubtreeIDs(ctx context.Context, key string, ids []int64) {
ttl := subtreeCacheTTL
if len(ids) == 0 {
ttl = subtreeEmptyTTL
}
if err := cache.SetJSON(ctx, key, ids, ttl); err != nil {
logrus.Warnf("set subtree cache failed, key=%s, err=%v", key, err)
}
}
func (r *DocumentRepository) fetchDocumentsByIDs(ids []int64) ([]*model.Document, error) {
if len(ids) == 0 {
return []*model.Document{}, nil
}
var docs []*model.Document
if err := r.db.Model(&model.Document{}).Where("id IN ?", ids).Find(&docs).Error; err != nil {
return nil, err
}
docMap := make(map[int64]*model.Document, len(docs))
for _, doc := range docs {
docMap[doc.ID] = doc
}
ordered := make([]*model.Document, 0, len(ids))
for _, id := range ids {
if doc, ok := docMap[id]; ok {
ordered = append(ordered, doc)
}
}
return ordered, nil
}
@@ -0,0 +1,84 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentUpdateOperation struct {
repo *DocumentRepository
doc *model.Document
updateFields map[string]bool
tx *gorm.DB
}
func (r *DocumentRepository) Update(doc *model.Document) *DocumentUpdateOperation {
return &DocumentUpdateOperation{
repo: r,
doc: doc,
updateFields: make(map[string]bool),
}
}
func (op *DocumentUpdateOperation) UpdateTitle() *DocumentUpdateOperation {
op.updateFields["title"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateSummary() *DocumentUpdateOperation {
op.updateFields["summary"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateContent() *DocumentUpdateOperation {
op.updateFields["content"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateParentID() *DocumentUpdateOperation {
op.updateFields["parent_id"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateKnowledgeID() *DocumentUpdateOperation {
op.updateFields["knowledge_id"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateContainerType() *DocumentUpdateOperation {
op.updateFields["container_type"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateIsPublic() *DocumentUpdateOperation {
op.updateFields["is_public"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateTags() *DocumentUpdateOperation {
op.updateFields["tags"] = true
return op
}
func (op *DocumentUpdateOperation) WithTx(tx *gorm.DB) *DocumentUpdateOperation {
op.tx = tx
return op
}
func (op *DocumentUpdateOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
query := op.tx.Model(op.doc)
if len(op.updateFields) > 0 {
fields := make([]string, 0, len(op.updateFields))
for field := range op.updateFields {
fields = append(fields, field)
}
query = query.Select(fields)
}
return query.Updates(*op.doc).Error
}
@@ -0,0 +1,48 @@
package document_repo
import (
"context"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/pkg/cache"
"github.com/XingfenD/yoresee_doc/pkg/key"
"gorm.io/gorm"
)
type DocumentUpdateContentByExternalIDOperation struct {
repo *DocumentRepository
externalID string
content string
tx *gorm.DB
}
func (r *DocumentRepository) UpdateContentByExternalID(externalID, content string) *DocumentUpdateContentByExternalIDOperation {
return &DocumentUpdateContentByExternalIDOperation{
repo: r,
externalID: externalID,
content: content,
}
}
func (op *DocumentUpdateContentByExternalIDOperation) WithTx(tx *gorm.DB) *DocumentUpdateContentByExternalIDOperation {
op.tx = tx
return op
}
func (op *DocumentUpdateContentByExternalIDOperation) Exec(ctx context.Context) error {
if op.tx == nil {
op.tx = op.repo.db
}
docModelCacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, op.externalID)
return cache.DoubleDelete(
context.Background(),
func() error {
return op.tx.WithContext(ctx).Model(&model.Document{}).
Where("external_id = ?", op.externalID).
Select("content").
Updates(map[string]interface{}{"content": op.content}).Error
},
docModelCacheKey,
)
}
@@ -0,0 +1,58 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentUpdatePathDepthOperation struct {
repo *DocumentRepository
docID int64
parentID int64
tx *gorm.DB
}
func (r *DocumentRepository) UpdatePathDepth(docID, parentID int64) *DocumentUpdatePathDepthOperation {
return &DocumentUpdatePathDepthOperation{
repo: r,
docID: docID,
parentID: parentID,
}
}
func (op *DocumentUpdatePathDepthOperation) WithTx(tx *gorm.DB) *DocumentUpdatePathDepthOperation {
op.tx = tx
return op
}
func (op *DocumentUpdatePathDepthOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
if op.parentID == 0 {
return op.tx.Exec(`
UPDATE document_metas
SET path = (id::text)::ltree, depth = 0
WHERE id = ? AND deleted_at IS NULL
`, op.docID).Error
}
type pathDepth struct {
Path string
Depth int
}
var parent pathDepth
if err := op.tx.Model(&model.Document{}).
Select("path, depth").
Where("id = ? AND deleted_at IS NULL", op.parentID).
Take(&parent).Error; err != nil {
return err
}
return op.tx.Exec(`
UPDATE document_metas
SET path = (?::ltree) || (id::text)::ltree, depth = ?
WHERE id = ? AND deleted_at IS NULL
`, parent.Path, parent.Depth+1, op.docID).Error
}
@@ -0,0 +1,43 @@
package document_version_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentVersionCreateOperation struct {
repo *DocumentVersionRepository
docVersion *model.DocumentVersion
tx *gorm.DB
}
func (repo *DocumentVersionRepository) Create(docVersion *model.DocumentVersion) *DocumentVersionCreateOperation {
return &DocumentVersionCreateOperation{
repo: repo,
docVersion: docVersion,
}
}
func (op *DocumentVersionCreateOperation) WithTx(tx *gorm.DB) *DocumentVersionCreateOperation {
op.tx = tx
return op
}
func (op *DocumentVersionCreateOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
var maxVersion int
err := op.tx.Model(&model.DocumentVersion{}).
Where("document_id = ?", op.docVersion.DocumentID).
Select("COALESCE(MAX(version), 0)").
Scan(&maxVersion).Error
if err != nil {
return err
}
op.docVersion.Version = maxVersion + 1
return op.tx.Create(op.docVersion).Error
}
@@ -0,0 +1,11 @@
package document_version_repo
import "gorm.io/gorm"
type DocumentVersionRepository struct {
db *gorm.DB
}
func NewDocumentVersionRepository(db *gorm.DB) *DocumentVersionRepository {
return &DocumentVersionRepository{db: db}
}
@@ -0,0 +1,13 @@
package document_version_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
)
func (r *DocumentVersionRepository) GetByDocumentIDAndVersion(documentID int64, version int) (*model.DocumentVersion, error) {
item := &model.DocumentVersion{}
if err := r.db.Where("document_id = ? AND version = ?", documentID, version).First(item).Error; err != nil {
return nil, err
}
return item, nil
}
@@ -0,0 +1,41 @@
package document_version_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentVersionGetLastedOperation struct {
repo *DocumentVersionRepository
docID int64
tx *gorm.DB
}
func (r *DocumentVersionRepository) GetLasted(docID int64) *DocumentVersionGetLastedOperation {
return &DocumentVersionGetLastedOperation{
repo: r,
docID: docID,
}
}
func (op *DocumentVersionGetLastedOperation) WithTx(tx *gorm.DB) *DocumentVersionGetLastedOperation {
op.tx = tx
return op
}
func (op *DocumentVersionGetLastedOperation) Exec() (int64, error) {
if op.tx == nil {
op.tx = op.repo.db
}
var maxVersion int64
err := op.tx.Model(&model.DocumentVersion{}).
Where("document_id = ?", op.docID).
Select("COALESCE(MAX(version), 0)").
Scan(&maxVersion).Error
if err != nil {
return 0, err
}
return maxVersion, nil
}
@@ -0,0 +1,19 @@
package document_version_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
)
func (r *DocumentVersionRepository) ListByDocumentID(documentID int64, offset, limit int) ([]*model.DocumentVersion, int64, error) {
var total int64
query := r.db.Model(&model.DocumentVersion{}).Where("document_id = ?", documentID)
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
items := make([]*model.DocumentVersion, 0)
if err := query.Order("version DESC").Offset(offset).Limit(limit).Find(&items).Error; err != nil {
return nil, 0, err
}
return items, total, nil
}
@@ -0,0 +1,86 @@
package doc_yjs_snapshot_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type DocumentYjsSnapshotRepository struct {
db *gorm.DB
}
func NewDocumentYjsSnapshotRepository(db *gorm.DB) *DocumentYjsSnapshotRepository {
return &DocumentYjsSnapshotRepository{db: db}
}
type DocumentYjsSnapshotGetOperation struct {
repo *DocumentYjsSnapshotRepository
docID int64
tx *gorm.DB
}
func (r *DocumentYjsSnapshotRepository) GetByDocID(docID int64) *DocumentYjsSnapshotGetOperation {
return &DocumentYjsSnapshotGetOperation{
repo: r,
docID: docID,
}
}
func (op *DocumentYjsSnapshotGetOperation) WithTx(tx *gorm.DB) *DocumentYjsSnapshotGetOperation {
op.tx = tx
return op
}
func (op *DocumentYjsSnapshotGetOperation) Exec() (*model.DocumentYjsSnapshot, error) {
if op.tx == nil {
op.tx = op.repo.db
}
var snapshot model.DocumentYjsSnapshot
if err := op.tx.First(&snapshot, "doc_id = ?", op.docID).Error; err != nil {
return nil, err
}
return &snapshot, nil
}
type DocumentYjsSnapshotSaveOperation struct {
repo *DocumentYjsSnapshotRepository
docID int64
state []byte
tx *gorm.DB
}
func (r *DocumentYjsSnapshotRepository) Save(docID int64, state []byte) *DocumentYjsSnapshotSaveOperation {
return &DocumentYjsSnapshotSaveOperation{
repo: r,
docID: docID,
state: state,
}
}
func (op *DocumentYjsSnapshotSaveOperation) WithTx(tx *gorm.DB) *DocumentYjsSnapshotSaveOperation {
op.tx = tx
return op
}
func (op *DocumentYjsSnapshotSaveOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
snapshot := &model.DocumentYjsSnapshot{
DocID: op.docID,
YjsState: op.state,
Version: 1,
}
return op.tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "doc_id"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"yjs_state": op.state,
"version": gorm.Expr("documents_yjs_snapshot.version + 1"),
"updated_at": gorm.Expr("NOW()"),
}),
}).Create(snapshot).Error
}
+51
View File
@@ -0,0 +1,51 @@
package repository
import (
"github.com/XingfenD/yoresee_doc/internal/repository/attachment_repo"
"github.com/XingfenD/yoresee_doc/internal/repository/comment_repo"
"github.com/XingfenD/yoresee_doc/internal/repository/document_repo"
"github.com/XingfenD/yoresee_doc/internal/repository/document_version_repo"
doc_yjs_snapshot_repo "github.com/XingfenD/yoresee_doc/internal/repository/document_yjs_snapshot_repo"
"github.com/XingfenD/yoresee_doc/internal/repository/invitation_repo"
"github.com/XingfenD/yoresee_doc/internal/repository/knowledge_base_repo"
"github.com/XingfenD/yoresee_doc/internal/repository/membership_repo"
"github.com/XingfenD/yoresee_doc/internal/repository/notification_repo"
"github.com/XingfenD/yoresee_doc/internal/repository/template_repo"
"github.com/XingfenD/yoresee_doc/internal/repository/user_repo"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type Repositories struct {
DB *gorm.DB
Redis *redis.Client
Document *document_repo.DocumentRepository
KnowledgeBase *knowledge_base_repo.KnowledgeBaseRepository
User *user_repo.UserRepository
Comment *comment_repo.CommentRepository
Invitation *invitation_repo.InvitationRepository
Membership *membership_repo.MembershipRepository
Template *template_repo.TemplateRepository
Attachment *attachment_repo.AttachmentRepository
DocumentVersion *document_version_repo.DocumentVersionRepository
Notification *notification_repo.NotificationRepository
DocumentYjsSnapshot *doc_yjs_snapshot_repo.DocumentYjsSnapshotRepository
}
func NewRepositories(db *gorm.DB, redis *redis.Client) *Repositories {
return &Repositories{
DB: db,
Redis: redis,
Document: document_repo.NewDocumentRepository(db, redis),
KnowledgeBase: knowledge_base_repo.NewKnowledgeBaseRepository(db, redis),
User: user_repo.NewUserRepository(db, redis),
Comment: comment_repo.NewCommentRepository(db),
Invitation: invitation_repo.NewInvitationRepository(db),
Membership: membership_repo.NewMembershipRepository(db),
Template: template_repo.NewTemplateRepository(db),
Attachment: attachment_repo.NewAttachmentRepository(db),
DocumentVersion: document_version_repo.NewDocumentVersionRepository(db),
Notification: notification_repo.NewNotificationRepository(db),
DocumentYjsSnapshot: doc_yjs_snapshot_repo.NewDocumentYjsSnapshotRepository(db),
}
}
+112
View File
@@ -0,0 +1,112 @@
package invitation_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type InvitationGetByIDOperation struct {
repo *InvitationRepository
id int64
tx *gorm.DB
}
func (r *InvitationRepository) GetByID(id int64) *InvitationGetByIDOperation {
return &InvitationGetByIDOperation{
repo: r,
id: id,
}
}
func (op *InvitationGetByIDOperation) WithTx(tx *gorm.DB) *InvitationGetByIDOperation {
op.tx = tx
return op
}
func (op *InvitationGetByIDOperation) Exec() (*model.Invitation, error) {
var invitation model.Invitation
var err error
if op.tx != nil {
err = op.tx.First(&invitation, op.id).Error
} else {
err = op.repo.db.First(&invitation, op.id).Error
}
return &invitation, err
}
type InvitationCreateOperation struct {
repo *InvitationRepository
invitation *model.Invitation
tx *gorm.DB
}
func (r *InvitationRepository) Create(invitation *model.Invitation) *InvitationCreateOperation {
return &InvitationCreateOperation{
repo: r,
invitation: invitation,
}
}
func (op *InvitationCreateOperation) WithTx(tx *gorm.DB) *InvitationCreateOperation {
op.tx = tx
return op
}
func (op *InvitationCreateOperation) Exec() error {
if op.tx != nil {
return op.tx.Create(op.invitation).Error
}
return op.repo.db.Create(op.invitation).Error
}
type InvitationUpdateOperation struct {
repo *InvitationRepository
invitation *model.Invitation
tx *gorm.DB
}
func (r *InvitationRepository) Update(invitation *model.Invitation) *InvitationUpdateOperation {
return &InvitationUpdateOperation{
repo: r,
invitation: invitation,
}
}
func (op *InvitationUpdateOperation) WithTx(tx *gorm.DB) *InvitationUpdateOperation {
op.tx = tx
return op
}
func (op *InvitationUpdateOperation) Exec() error {
if op.tx != nil {
return op.tx.Save(op.invitation).Error
}
return op.repo.db.Save(op.invitation).Error
}
type InvitationDeleteOperation struct {
repo *InvitationRepository
id int64
tx *gorm.DB
}
func (r *InvitationRepository) Delete(id int64) *InvitationDeleteOperation {
return &InvitationDeleteOperation{
repo: r,
id: id,
}
}
func (op *InvitationDeleteOperation) WithTx(tx *gorm.DB) *InvitationDeleteOperation {
op.tx = tx
return op
}
func (op *InvitationDeleteOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
return op.tx.Delete(&model.Invitation{}, op.id).Error
}
@@ -0,0 +1,37 @@
package invitation_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type InvitationGetByCodeOperation struct {
repo *InvitationRepository
code string
tx *gorm.DB
}
func (r *InvitationRepository) GetByCode(code string) *InvitationGetByCodeOperation {
return &InvitationGetByCodeOperation{
repo: r,
code: code,
}
}
func (op *InvitationGetByCodeOperation) WithTx(tx *gorm.DB) *InvitationGetByCodeOperation {
op.tx = tx
return op
}
func (op *InvitationGetByCodeOperation) Exec() (*model.Invitation, error) {
var invitation model.Invitation
var err error
if op.tx != nil {
err = op.tx.Where("code = ?", op.code).First(&invitation).Error
} else {
err = op.repo.db.Where("code = ?", op.code).First(&invitation).Error
}
return &invitation, err
}
@@ -0,0 +1,11 @@
package invitation_repo
import "gorm.io/gorm"
type InvitationRepository struct {
db *gorm.DB
}
func NewInvitationRepository(db *gorm.DB) *InvitationRepository {
return &InvitationRepository{db: db}
}
+234
View File
@@ -0,0 +1,234 @@
package invitation_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type InvitationListOperation struct {
repo *InvitationRepository
m *model.Invitation
creatorID *int64
keyword *string
maxUsedCnt *int64
expiresAtStart *string
expiresAtEnd *string
createdAtStart *string
createdAtEnd *string
disabled *bool
sortField string
sortDesc bool
page int
pageSize int
tx *gorm.DB
}
func (r *InvitationRepository) List(m *model.Invitation) *InvitationListOperation {
return &InvitationListOperation{
repo: r,
m: m,
}
}
func (op *InvitationListOperation) WithTx(tx *gorm.DB) *InvitationListOperation {
op.tx = tx
return op
}
func (op *InvitationListOperation) WithCreatorID(creatorID *int64) *InvitationListOperation {
op.creatorID = creatorID
return op
}
func (op *InvitationListOperation) WithKeyword(keyword *string) *InvitationListOperation {
op.keyword = keyword
return op
}
func (op *InvitationListOperation) WithMaxUsedCnt(maxUsedCnt *int64) *InvitationListOperation {
op.maxUsedCnt = maxUsedCnt
return op
}
func (op *InvitationListOperation) WithExpiresAtRange(start, end *string) *InvitationListOperation {
op.expiresAtStart = start
op.expiresAtEnd = end
return op
}
func (op *InvitationListOperation) WithCreatedAtRange(start, end *string) *InvitationListOperation {
op.createdAtStart = start
op.createdAtEnd = end
return op
}
func (op *InvitationListOperation) WithDisabled(disabled *bool) *InvitationListOperation {
op.disabled = disabled
return op
}
func (op *InvitationListOperation) WithSort(field string, desc bool) *InvitationListOperation {
op.sortField = field
op.sortDesc = desc
return op
}
func (op *InvitationListOperation) WithPagination(page, pageSize int) *InvitationListOperation {
op.page = page
op.pageSize = pageSize
return op
}
func (op *InvitationListOperation) Exec() ([]model.Invitation, error) {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
dbQuery := db.Model(&model.Invitation{})
if op.m != nil {
dbQuery = dbQuery.Where(op.m)
}
if op.creatorID != nil {
dbQuery = dbQuery.Where("created_by = ?", *op.creatorID)
}
if op.keyword != nil && *op.keyword != "" {
dbQuery = dbQuery.Where("code ILIKE ?", "%"+*op.keyword+"%")
}
if op.maxUsedCnt != nil {
dbQuery = dbQuery.Where("max_used_cnt = ?", *op.maxUsedCnt)
}
if op.expiresAtStart != nil && *op.expiresAtStart != "" {
dbQuery = dbQuery.Where("expires_at >= ?", *op.expiresAtStart)
}
if op.expiresAtEnd != nil && *op.expiresAtEnd != "" {
dbQuery = dbQuery.Where("expires_at <= ?", *op.expiresAtEnd)
}
if op.createdAtStart != nil && *op.createdAtStart != "" {
dbQuery = dbQuery.Where("created_at >= ?", *op.createdAtStart)
}
if op.createdAtEnd != nil && *op.createdAtEnd != "" {
dbQuery = dbQuery.Where("created_at <= ?", *op.createdAtEnd)
}
if op.disabled != nil {
dbQuery = dbQuery.Where("disabled = ?", *op.disabled)
}
sortField := op.sortField
if sortField == "" {
sortField = "created_at"
}
orderDirection := "ASC"
if op.sortDesc {
orderDirection = "DESC"
}
dbQuery = dbQuery.Order(sortField + " " + orderDirection)
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
dbQuery = dbQuery.Offset(offset).Limit(pageSize)
var invitations []model.Invitation
err := dbQuery.Find(&invitations).Error
if err != nil {
return nil, err
}
return invitations, nil
}
func (op *InvitationListOperation) ExecWithTotal() ([]model.Invitation, int64, error) {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
dbQuery := db.Model(&model.Invitation{})
if op.m != nil {
dbQuery = dbQuery.Where(op.m)
}
if op.creatorID != nil {
dbQuery = dbQuery.Where("created_by = ?", *op.creatorID)
}
if op.keyword != nil && *op.keyword != "" {
dbQuery = dbQuery.Where("code ILIKE ?", "%"+*op.keyword+"%")
}
if op.maxUsedCnt != nil {
dbQuery = dbQuery.Where("max_used_cnt = ?", *op.maxUsedCnt)
}
if op.expiresAtStart != nil && *op.expiresAtStart != "" {
dbQuery = dbQuery.Where("expires_at >= ?", *op.expiresAtStart)
}
if op.expiresAtEnd != nil && *op.expiresAtEnd != "" {
dbQuery = dbQuery.Where("expires_at <= ?", *op.expiresAtEnd)
}
if op.createdAtStart != nil && *op.createdAtStart != "" {
dbQuery = dbQuery.Where("created_at >= ?", *op.createdAtStart)
}
if op.createdAtEnd != nil && *op.createdAtEnd != "" {
dbQuery = dbQuery.Where("created_at <= ?", *op.createdAtEnd)
}
if op.disabled != nil {
dbQuery = dbQuery.Where("disabled = ?", *op.disabled)
}
var total int64
err := dbQuery.Count(&total).Error
if err != nil {
return nil, 0, err
}
sortField := op.sortField
if sortField == "" {
sortField = "created_at"
}
orderDirection := "ASC"
if op.sortDesc {
orderDirection = "DESC"
}
dbQuery = dbQuery.Order(sortField + " " + orderDirection)
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
dbQuery = dbQuery.Offset(offset).Limit(pageSize)
var invitations []model.Invitation
err = dbQuery.Find(&invitations).Error
if err != nil {
return nil, 0, err
}
return invitations, total, nil
}
@@ -0,0 +1,142 @@
package invitation_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type InvitationRecordCreateOperation struct {
repo *InvitationRepository
record *model.InvitationRecord
tx *gorm.DB
}
func (r *InvitationRepository) CreateRecord(record *model.InvitationRecord) *InvitationRecordCreateOperation {
return &InvitationRecordCreateOperation{
repo: r,
record: record,
}
}
func (op *InvitationRecordCreateOperation) WithTx(tx *gorm.DB) *InvitationRecordCreateOperation {
op.tx = tx
return op
}
func (op *InvitationRecordCreateOperation) Exec() error {
if op.tx != nil {
return op.tx.Create(op.record).Error
}
return op.repo.db.Create(op.record).Error
}
type InvitationRecordListOperation struct {
repo *InvitationRepository
code *string
status *string
usedAtStart *string
usedAtEnd *string
creatorID *int64
keyword *string
page int
pageSize int
tx *gorm.DB
}
func (r *InvitationRepository) ListRecords() *InvitationRecordListOperation {
return &InvitationRecordListOperation{
repo: r,
}
}
func (op *InvitationRecordListOperation) WithTx(tx *gorm.DB) *InvitationRecordListOperation {
op.tx = tx
return op
}
func (op *InvitationRecordListOperation) WithCode(code *string) *InvitationRecordListOperation {
op.code = code
return op
}
func (op *InvitationRecordListOperation) WithStatus(status *string) *InvitationRecordListOperation {
op.status = status
return op
}
func (op *InvitationRecordListOperation) WithUsedAtRange(start, end *string) *InvitationRecordListOperation {
op.usedAtStart = start
op.usedAtEnd = end
return op
}
func (op *InvitationRecordListOperation) WithCreatorID(creatorID *int64) *InvitationRecordListOperation {
op.creatorID = creatorID
return op
}
func (op *InvitationRecordListOperation) WithKeyword(keyword *string) *InvitationRecordListOperation {
op.keyword = keyword
return op
}
func (op *InvitationRecordListOperation) WithPagination(page, pageSize int) *InvitationRecordListOperation {
op.page = page
op.pageSize = pageSize
return op
}
func (op *InvitationRecordListOperation) ExecWithTotal() ([]model.InvitationRecord, int64, error) {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
query := db.Model(&model.InvitationRecord{})
if op.creatorID != nil {
query = query.Joins("JOIN invitations ON invitations.code = invitation_records.code").
Where("invitations.created_by = ?", *op.creatorID)
}
if op.code != nil && *op.code != "" {
query = query.Where("code = ?", *op.code)
}
if op.keyword != nil && *op.keyword != "" {
keyword := "%" + *op.keyword + "%"
query = query.Where("invitation_records.code ILIKE ? OR invitation_records.used_by ILIKE ?", keyword, keyword)
}
if op.status != nil && *op.status != "" {
query = query.Where("status = ?", *op.status)
}
if op.usedAtStart != nil && *op.usedAtStart != "" {
query = query.Where("used_at >= ?", *op.usedAtStart)
}
if op.usedAtEnd != nil && *op.usedAtEnd != "" {
query = query.Where("used_at <= ?", *op.usedAtEnd)
}
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 > 200 {
pageSize = 200
}
offset := (page - 1) * pageSize
query = query.Order("used_at DESC").Offset(offset).Limit(pageSize)
var records []model.InvitationRecord
if err := query.Find(&records).Error; err != nil {
return nil, 0, err
}
return records, total, nil
}
@@ -0,0 +1,48 @@
package invitation_repo
import (
"time"
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type InvitationValidateAndUseOperation struct {
repo *InvitationRepository
code string
tx *gorm.DB
}
func (r *InvitationRepository) ValidateAndUse(code string) *InvitationValidateAndUseOperation {
return &InvitationValidateAndUseOperation{
repo: r,
code: code,
}
}
func (op *InvitationValidateAndUseOperation) WithTx(tx *gorm.DB) *InvitationValidateAndUseOperation {
op.tx = tx
return op
}
func (op *InvitationValidateAndUseOperation) Exec() (bool, error) {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
now := time.Now()
result := db.Model(&model.Invitation{}).
Where("code = ? AND deleted_at IS NULL AND disabled = ?", op.code, false).
Where("(expires_at IS NULL OR expires_at > ?)", now).
Where("(max_used_cnt IS NULL OR used_cnt < max_used_cnt)").
UpdateColumn("used_cnt", gorm.Expr("used_cnt + ?", 1))
if result.Error != nil {
return false, result.Error
}
if result.RowsAffected == 0 {
return false, nil
}
return true, nil
}
@@ -0,0 +1,32 @@
package knowledge_base_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type CreateKnowledgeBaseOperation struct {
repo *KnowledgeBaseRepository
knowledgeBase *model.KnowledgeBase
tx *gorm.DB
}
func (r *KnowledgeBaseRepository) Create(knowledgeBase *model.KnowledgeBase) (op *CreateKnowledgeBaseOperation) {
return &CreateKnowledgeBaseOperation{
repo: r,
knowledgeBase: knowledgeBase,
}
}
func (op *CreateKnowledgeBaseOperation) WithTx(tx *gorm.DB) *CreateKnowledgeBaseOperation {
op.tx = tx
return op
}
func (op *CreateKnowledgeBaseOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
return op.tx.Create(op.knowledgeBase).Error
}
@@ -0,0 +1,38 @@
package knowledge_base_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type CreateRecentKnowledgeBaseOperation struct {
repo *KnowledgeBaseRepository
m *model.RecentKnowledgeBase
tx *gorm.DB
}
func (r *KnowledgeBaseRepository) CreateRecentKnowledgeBase(m *model.RecentKnowledgeBase) *CreateRecentKnowledgeBaseOperation {
return &CreateRecentKnowledgeBaseOperation{
repo: r,
m: m,
}
}
func (op *CreateRecentKnowledgeBaseOperation) WithTx(tx *gorm.DB) {
op.tx = tx
}
func (op *CreateRecentKnowledgeBaseOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
return op.tx.Clauses(clause.OnConflict{
Columns: []clause.Column{
{Name: "user_id"},
{Name: "knowledge_base_id"},
},
DoUpdates: clause.AssignmentColumns([]string{"accessed_at"}),
}).Create(op.m).Error
}
@@ -0,0 +1,32 @@
package knowledge_base_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DeleteKnowledgeBaseOperation struct {
repo *KnowledgeBaseRepository
knowledgeBase *model.KnowledgeBase
tx *gorm.DB
}
func (r *KnowledgeBaseRepository) Delete(knowledgeBase *model.KnowledgeBase) (op *DeleteKnowledgeBaseOperation) {
return &DeleteKnowledgeBaseOperation{
repo: r,
knowledgeBase: knowledgeBase,
}
}
func (op *DeleteKnowledgeBaseOperation) WithTx(tx *gorm.DB) *DeleteKnowledgeBaseOperation {
op.tx = tx
return op
}
func (op *DeleteKnowledgeBaseOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
err := op.tx.Delete(op.knowledgeBase).Error
return err
}
@@ -0,0 +1,56 @@
package knowledge_base_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 GetKnowledgeBaseByExternalIDOperation struct {
repo *KnowledgeBaseRepository
externalID string
tx *gorm.DB
}
func (r *KnowledgeBaseRepository) GetByExternalID(externalID string) (op *GetKnowledgeBaseByExternalIDOperation) {
return &GetKnowledgeBaseByExternalIDOperation{
repo: r,
externalID: externalID,
}
}
func (op *GetKnowledgeBaseByExternalIDOperation) WithTx(tx *gorm.DB) *GetKnowledgeBaseByExternalIDOperation {
op.tx = tx
return op
}
func (op *GetKnowledgeBaseByExternalIDOperation) query(db *gorm.DB) (*model.KnowledgeBase, error) {
var knowledgeBase model.KnowledgeBase
err := db.First(&knowledgeBase, "external_id = ?", op.externalID).Error
return &knowledgeBase, err
}
func (op *GetKnowledgeBaseByExternalIDOperation) Exec() (*model.KnowledgeBase, error) {
if op.tx != nil {
return op.query(op.tx)
}
knowledgeBaseCacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_KnowledgeBase, op.externalID)
knowledgeBase, err := cache_loader.NewCacheLoadOperation[model.KnowledgeBase](&op.repo.Loader).
WithDBLoader(func() (*model.KnowledgeBase, error) {
return op.query(op.repo.db)
}).WithDefaultKeyAndParser(knowledgeBaseCacheKey, nil).
Exec(context.Background())
if err != nil {
logrus.Errorf("load data failed for GetKnowledgeBaseByExternalIDOperation: %+v", err)
return nil, err
}
return knowledgeBase, nil
}
@@ -0,0 +1,32 @@
package knowledge_base_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type KnowledgeBaseGetByIDOperation struct {
repo *KnowledgeBaseRepository
id int64
tx *gorm.DB
}
func (r *KnowledgeBaseRepository) GetByID(id int64) (op *KnowledgeBaseGetByIDOperation) {
return &KnowledgeBaseGetByIDOperation{
repo: r,
id: id,
}
}
func (op *KnowledgeBaseGetByIDOperation) WithTx(tx *gorm.DB) *KnowledgeBaseGetByIDOperation {
op.tx = tx
return op
}
func (op *KnowledgeBaseGetByIDOperation) Exec() (knowledgeBase *model.KnowledgeBase, err error) {
if op.tx == nil {
op.tx = op.repo.db
}
err = op.tx.First(knowledgeBase, "id = ?", op.id).Error
return
}
@@ -0,0 +1,36 @@
package knowledge_base_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type KnowledgeBaseGetIDByExternalIDOperation struct {
repo *KnowledgeBaseRepository
externalID string
tx *gorm.DB
}
func (r *KnowledgeBaseRepository) GetIDByExternalID(externalID string) (op *KnowledgeBaseGetIDByExternalIDOperation) {
return &KnowledgeBaseGetIDByExternalIDOperation{
repo: r,
externalID: externalID,
}
}
func (op *KnowledgeBaseGetIDByExternalIDOperation) WithTx(tx *gorm.DB) *KnowledgeBaseGetIDByExternalIDOperation {
op.tx = tx
return op
}
func (op *KnowledgeBaseGetIDByExternalIDOperation) Exec() (int64, error) {
var id int64
if op.tx == nil {
op.tx = op.repo.db
}
err := op.tx.Model(&model.KnowledgeBase{}).Where("external_id = ?", op.externalID).Pluck("id", &id).Error
if err != nil {
return 0, err
}
return id, nil
}

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