migrate from github
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Vendored
+144
@@ -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
|
||||
}
|
||||
Vendored
+27
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
// }
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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 = ®istry{
|
||||
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{}) }
|
||||
@@ -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 = ®istry{
|
||||
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()
|
||||
}
|
||||
@@ -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 = ®istry{
|
||||
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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user