feat: add tenant and user management with migrations, handlers, and tests
- Implemented tenant and user management features including: - Tenant creation and management with associated migrations. - User creation and management with associated migrations. - Tenant membership management with associated migrations. - Platform user roles management with associated migrations. - Quota management with associated migrations. - Article and template management with associated migrations. - Added HTTP handlers for templates and workspaces. - Created tests for protected and public routes. - Introduced a script to check tenant scope in SQL queries. - Documented task plan for backend completion and frontend foundation.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
|
||||
type CreateArticleInput struct {
|
||||
TenantID int64
|
||||
SourceType string
|
||||
TemplateID *int64
|
||||
}
|
||||
|
||||
type CreateArticleVersionInput struct {
|
||||
ArticleID int64
|
||||
VersionNo int
|
||||
Title string
|
||||
HTMLContent string
|
||||
MarkdownContent string
|
||||
WordCount int
|
||||
SourceLabel string
|
||||
}
|
||||
|
||||
type ArticleRepository interface {
|
||||
CreateArticle(ctx context.Context, input CreateArticleInput) (int64, error)
|
||||
CreateArticleVersion(ctx context.Context, input CreateArticleVersionInput) (int64, error)
|
||||
UpdateArticleCurrentVersion(ctx context.Context, articleID, tenantID, versionID int64) error
|
||||
UpdateArticleGenerateStatus(ctx context.Context, articleID, tenantID int64, status string) error
|
||||
}
|
||||
|
||||
type articleRepository struct {
|
||||
q generated.Querier
|
||||
}
|
||||
|
||||
func NewArticleRepository(db generated.DBTX) ArticleRepository {
|
||||
return &articleRepository{q: newQuerier(db)}
|
||||
}
|
||||
|
||||
func (r *articleRepository) CreateArticle(ctx context.Context, input CreateArticleInput) (int64, error) {
|
||||
return r.q.CreateArticle(ctx, generated.CreateArticleParams{
|
||||
TenantID: input.TenantID,
|
||||
SourceType: input.SourceType,
|
||||
TemplateID: pgInt8(input.TemplateID),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *articleRepository) CreateArticleVersion(ctx context.Context, input CreateArticleVersionInput) (int64, error) {
|
||||
return r.q.CreateArticleVersion(ctx, generated.CreateArticleVersionParams{
|
||||
ArticleID: input.ArticleID,
|
||||
VersionNo: int32(input.VersionNo),
|
||||
Title: pgText(&input.Title),
|
||||
HtmlContent: pgText(&input.HTMLContent),
|
||||
MarkdownContent: pgText(&input.MarkdownContent),
|
||||
WordCount: int32(input.WordCount),
|
||||
SourceLabel: pgText(&input.SourceLabel),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *articleRepository) UpdateArticleCurrentVersion(ctx context.Context, articleID, tenantID, versionID int64) error {
|
||||
return r.q.UpdateArticleCurrentVersion(ctx, generated.UpdateArticleCurrentVersionParams{
|
||||
VersionID: pgInt8(&versionID),
|
||||
ID: articleID,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *articleRepository) UpdateArticleGenerateStatus(ctx context.Context, articleID, tenantID int64, status string) error {
|
||||
return r.q.UpdateArticleGenerateStatus(ctx, generated.UpdateArticleGenerateStatusParams{
|
||||
Status: status,
|
||||
ID: articleID,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
|
||||
type AuditLogInput struct {
|
||||
OperatorID int64
|
||||
TenantID *int64
|
||||
Module string
|
||||
Action string
|
||||
BeforeJSON []byte
|
||||
AfterJSON []byte
|
||||
Result *string
|
||||
}
|
||||
|
||||
type GenerationTaskInput struct {
|
||||
TenantID int64
|
||||
ArticleID *int64
|
||||
QuotaReservationID *int64
|
||||
TaskType string
|
||||
InputParamsJSON []byte
|
||||
}
|
||||
|
||||
type GenerationTaskStatusInput struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
Status string
|
||||
ErrorMessage *string
|
||||
StartedAt *time.Time
|
||||
CompletedAt *time.Time
|
||||
}
|
||||
|
||||
type AuditRepository interface {
|
||||
CreateAuditLog(ctx context.Context, input AuditLogInput) error
|
||||
CreateGenerationTask(ctx context.Context, input GenerationTaskInput) (int64, error)
|
||||
UpdateGenerationTaskStatus(ctx context.Context, input GenerationTaskStatusInput) error
|
||||
}
|
||||
|
||||
type auditRepository struct {
|
||||
q generated.Querier
|
||||
}
|
||||
|
||||
func NewAuditRepository(db generated.DBTX) AuditRepository {
|
||||
return &auditRepository{q: newQuerier(db)}
|
||||
}
|
||||
|
||||
func (r *auditRepository) CreateAuditLog(ctx context.Context, input AuditLogInput) error {
|
||||
return r.q.CreateAuditLog(ctx, generated.CreateAuditLogParams{
|
||||
OperatorID: input.OperatorID,
|
||||
TenantID: pgInt8(input.TenantID),
|
||||
Module: input.Module,
|
||||
Action: input.Action,
|
||||
BeforeJson: input.BeforeJSON,
|
||||
AfterJson: input.AfterJSON,
|
||||
Result: pgText(input.Result),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *auditRepository) CreateGenerationTask(ctx context.Context, input GenerationTaskInput) (int64, error) {
|
||||
return r.q.CreateGenerationTask(ctx, generated.CreateGenerationTaskParams{
|
||||
TenantID: input.TenantID,
|
||||
ArticleID: pgInt8(input.ArticleID),
|
||||
QuotaReservationID: pgInt8(input.QuotaReservationID),
|
||||
TaskType: input.TaskType,
|
||||
InputParamsJson: input.InputParamsJSON,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *auditRepository) UpdateGenerationTaskStatus(ctx context.Context, input GenerationTaskStatusInput) error {
|
||||
return r.q.UpdateGenerationTaskStatus(ctx, generated.UpdateGenerationTaskStatusParams{
|
||||
Status: input.Status,
|
||||
ErrorMessage: pgText(input.ErrorMessage),
|
||||
StartedAt: pgTimestamp(input.StartedAt),
|
||||
CompletedAt: pgTimestamp(input.CompletedAt),
|
||||
ID: input.ID,
|
||||
TenantID: input.TenantID,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
|
||||
type UserRecord struct {
|
||||
ID int64
|
||||
Email string
|
||||
Phone *string
|
||||
PasswordHash string
|
||||
Name string
|
||||
AvatarURL *string
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type MembershipRecord struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
UserID int64
|
||||
TenantRole string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type AuthRepository interface {
|
||||
GetUserByEmail(ctx context.Context, email string) (*UserRecord, error)
|
||||
GetUserByID(ctx context.Context, id int64) (*UserRecord, error)
|
||||
GetTenantMembership(ctx context.Context, userID int64) (*MembershipRecord, error)
|
||||
GetTenantMembershipByTenantAndUser(ctx context.Context, tenantID, userID int64) (*MembershipRecord, error)
|
||||
}
|
||||
|
||||
type authRepository struct {
|
||||
q generated.Querier
|
||||
}
|
||||
|
||||
func NewAuthRepository(db generated.DBTX) AuthRepository {
|
||||
return &authRepository{q: newQuerier(db)}
|
||||
}
|
||||
|
||||
func (r *authRepository) GetUserByEmail(ctx context.Context, email string) (*UserRecord, error) {
|
||||
row, err := r.q.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UserRecord{
|
||||
ID: row.ID,
|
||||
Email: row.Email,
|
||||
Phone: nullableText(row.Phone),
|
||||
PasswordHash: row.PasswordHash,
|
||||
Name: row.Name,
|
||||
AvatarURL: nullableText(row.AvatarUrl),
|
||||
Status: row.Status,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *authRepository) GetUserByID(ctx context.Context, id int64) (*UserRecord, error) {
|
||||
row, err := r.q.GetUserByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UserRecord{
|
||||
ID: row.ID,
|
||||
Email: row.Email,
|
||||
Phone: nullableText(row.Phone),
|
||||
PasswordHash: row.PasswordHash,
|
||||
Name: row.Name,
|
||||
AvatarURL: nullableText(row.AvatarUrl),
|
||||
Status: row.Status,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *authRepository) GetTenantMembership(ctx context.Context, userID int64) (*MembershipRecord, error) {
|
||||
row, err := r.q.GetTenantMembership(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MembershipRecord{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
UserID: row.UserID,
|
||||
TenantRole: row.TenantRole,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *authRepository) GetTenantMembershipByTenantAndUser(ctx context.Context, tenantID, userID int64) (*MembershipRecord, error) {
|
||||
row, err := r.q.GetTenantMembershipByTenantAndUser(ctx, generated.GetTenantMembershipByTenantAndUserParams{
|
||||
TenantID: tenantID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MembershipRecord{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
UserID: row.UserID,
|
||||
TenantRole: row.TenantRole,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
|
||||
func newQuerier(db generated.DBTX) generated.Querier {
|
||||
return generated.New(db)
|
||||
}
|
||||
|
||||
func nullableText(value pgtype.Text) *string {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
}
|
||||
text := value.String
|
||||
return &text
|
||||
}
|
||||
|
||||
func nullableInt64(value pgtype.Int8) *int64 {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
}
|
||||
number := value.Int64
|
||||
return &number
|
||||
}
|
||||
|
||||
func intFromInt4(value pgtype.Int4) int {
|
||||
if !value.Valid {
|
||||
return 0
|
||||
}
|
||||
return int(value.Int32)
|
||||
}
|
||||
|
||||
func timeFromTimestamp(value pgtype.Timestamptz) time.Time {
|
||||
if !value.Valid {
|
||||
return time.Time{}
|
||||
}
|
||||
return value.Time
|
||||
}
|
||||
|
||||
func pgText(value *string) pgtype.Text {
|
||||
if value == nil {
|
||||
return pgtype.Text{}
|
||||
}
|
||||
return pgtype.Text{String: *value, Valid: true}
|
||||
}
|
||||
|
||||
func pgInt8(value *int64) pgtype.Int8 {
|
||||
if value == nil {
|
||||
return pgtype.Int8{}
|
||||
}
|
||||
return pgtype.Int8{Int64: *value, Valid: true}
|
||||
}
|
||||
|
||||
func pgTimestamp(value *time.Time) pgtype.Timestamptz {
|
||||
if value == nil {
|
||||
return pgtype.Timestamptz{}
|
||||
}
|
||||
return pgtype.Timestamptz{Time: *value, Valid: true}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: article.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const countArticles = `-- name: CountArticles :one
|
||||
SELECT COUNT(*)
|
||||
FROM articles a
|
||||
LEFT JOIN article_versions av ON av.id = a.current_version_id
|
||||
WHERE a.tenant_id = $1
|
||||
AND a.deleted_at IS NULL
|
||||
AND ($2::text IS NULL OR a.generate_status = $2)
|
||||
AND ($3::text IS NULL OR a.publish_status = $3)
|
||||
AND ($4::text IS NULL OR a.source_type = $4)
|
||||
AND ($5::bigint IS NULL OR a.template_id = $5)
|
||||
AND ($6::text IS NULL OR av.title ILIKE '%' || $6 || '%')
|
||||
`
|
||||
|
||||
type CountArticlesParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
GenerateStatus pgtype.Text `json:"generate_status"`
|
||||
PublishStatus pgtype.Text `json:"publish_status"`
|
||||
SourceType pgtype.Text `json:"source_type"`
|
||||
TemplateID pgtype.Int8 `json:"template_id"`
|
||||
Keyword pgtype.Text `json:"keyword"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountArticles(ctx context.Context, arg CountArticlesParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countArticles,
|
||||
arg.TenantID,
|
||||
arg.GenerateStatus,
|
||||
arg.PublishStatus,
|
||||
arg.SourceType,
|
||||
arg.TemplateID,
|
||||
arg.Keyword,
|
||||
)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const createArticle = `-- name: CreateArticle :one
|
||||
INSERT INTO articles (tenant_id, source_type, template_id, generate_status, publish_status)
|
||||
VALUES ($1, $2, $3, 'pending', 'unpublished')
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type CreateArticleParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
SourceType string `json:"source_type"`
|
||||
TemplateID pgtype.Int8 `json:"template_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateArticle(ctx context.Context, arg CreateArticleParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, createArticle, arg.TenantID, arg.SourceType, arg.TemplateID)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const createArticleVersion = `-- name: CreateArticleVersion :one
|
||||
INSERT INTO article_versions (article_id, version_no, title, html_content, markdown_content, word_count, source_label)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type CreateArticleVersionParams struct {
|
||||
ArticleID int64 `json:"article_id"`
|
||||
VersionNo int32 `json:"version_no"`
|
||||
Title pgtype.Text `json:"title"`
|
||||
HtmlContent pgtype.Text `json:"html_content"`
|
||||
MarkdownContent pgtype.Text `json:"markdown_content"`
|
||||
WordCount int32 `json:"word_count"`
|
||||
SourceLabel pgtype.Text `json:"source_label"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateArticleVersion(ctx context.Context, arg CreateArticleVersionParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, createArticleVersion,
|
||||
arg.ArticleID,
|
||||
arg.VersionNo,
|
||||
arg.Title,
|
||||
arg.HtmlContent,
|
||||
arg.MarkdownContent,
|
||||
arg.WordCount,
|
||||
arg.SourceLabel,
|
||||
)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const getArticleByID = `-- name: GetArticleByID :one
|
||||
SELECT a.id, a.tenant_id, a.source_type, a.template_id, a.current_version_id,
|
||||
a.generate_status, a.publish_status, a.created_at, a.updated_at,
|
||||
av.title, av.html_content, av.markdown_content, av.word_count, av.source_label, av.version_no,
|
||||
t.template_name
|
||||
FROM articles a
|
||||
LEFT JOIN article_versions av ON av.id = a.current_version_id
|
||||
LEFT JOIN article_templates t ON t.id = a.template_id
|
||||
WHERE a.id = $1 AND a.tenant_id = $2 AND a.deleted_at IS NULL
|
||||
`
|
||||
|
||||
type GetArticleByIDParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
type GetArticleByIDRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
SourceType string `json:"source_type"`
|
||||
TemplateID pgtype.Int8 `json:"template_id"`
|
||||
CurrentVersionID pgtype.Int8 `json:"current_version_id"`
|
||||
GenerateStatus string `json:"generate_status"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
Title pgtype.Text `json:"title"`
|
||||
HtmlContent pgtype.Text `json:"html_content"`
|
||||
MarkdownContent pgtype.Text `json:"markdown_content"`
|
||||
WordCount pgtype.Int4 `json:"word_count"`
|
||||
SourceLabel pgtype.Text `json:"source_label"`
|
||||
VersionNo pgtype.Int4 `json:"version_no"`
|
||||
TemplateName pgtype.Text `json:"template_name"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetArticleByID(ctx context.Context, arg GetArticleByIDParams) (GetArticleByIDRow, error) {
|
||||
row := q.db.QueryRow(ctx, getArticleByID, arg.ID, arg.TenantID)
|
||||
var i GetArticleByIDRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.SourceType,
|
||||
&i.TemplateID,
|
||||
&i.CurrentVersionID,
|
||||
&i.GenerateStatus,
|
||||
&i.PublishStatus,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Title,
|
||||
&i.HtmlContent,
|
||||
&i.MarkdownContent,
|
||||
&i.WordCount,
|
||||
&i.SourceLabel,
|
||||
&i.VersionNo,
|
||||
&i.TemplateName,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getArticleVersions = `-- name: GetArticleVersions :many
|
||||
SELECT id, article_id, version_no, title, word_count, source_label, created_at
|
||||
FROM article_versions
|
||||
WHERE article_id = $1
|
||||
ORDER BY version_no DESC
|
||||
`
|
||||
|
||||
type GetArticleVersionsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
ArticleID int64 `json:"article_id"`
|
||||
VersionNo int32 `json:"version_no"`
|
||||
Title pgtype.Text `json:"title"`
|
||||
WordCount int32 `json:"word_count"`
|
||||
SourceLabel pgtype.Text `json:"source_label"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetArticleVersions(ctx context.Context, articleID int64) ([]GetArticleVersionsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getArticleVersions, articleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetArticleVersionsRow
|
||||
for rows.Next() {
|
||||
var i GetArticleVersionsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ArticleID,
|
||||
&i.VersionNo,
|
||||
&i.Title,
|
||||
&i.WordCount,
|
||||
&i.SourceLabel,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listArticles = `-- name: ListArticles :many
|
||||
SELECT a.id, a.tenant_id, a.source_type, a.template_id,
|
||||
a.generate_status, a.publish_status, a.created_at, a.updated_at,
|
||||
av.title, av.word_count, av.source_label,
|
||||
t.template_name
|
||||
FROM articles a
|
||||
LEFT JOIN article_versions av ON av.id = a.current_version_id
|
||||
LEFT JOIN article_templates t ON t.id = a.template_id
|
||||
WHERE a.tenant_id = $1
|
||||
AND a.deleted_at IS NULL
|
||||
AND ($2::text IS NULL OR a.generate_status = $2)
|
||||
AND ($3::text IS NULL OR a.publish_status = $3)
|
||||
AND ($4::text IS NULL OR a.source_type = $4)
|
||||
AND ($5::bigint IS NULL OR a.template_id = $5)
|
||||
AND ($6::text IS NULL OR av.title ILIKE '%' || $6 || '%')
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT $8 OFFSET $7
|
||||
`
|
||||
|
||||
type ListArticlesParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
GenerateStatus pgtype.Text `json:"generate_status"`
|
||||
PublishStatus pgtype.Text `json:"publish_status"`
|
||||
SourceType pgtype.Text `json:"source_type"`
|
||||
TemplateID pgtype.Int8 `json:"template_id"`
|
||||
Keyword pgtype.Text `json:"keyword"`
|
||||
PageOffset int32 `json:"page_offset"`
|
||||
PageSize int32 `json:"page_size"`
|
||||
}
|
||||
|
||||
type ListArticlesRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
SourceType string `json:"source_type"`
|
||||
TemplateID pgtype.Int8 `json:"template_id"`
|
||||
GenerateStatus string `json:"generate_status"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
Title pgtype.Text `json:"title"`
|
||||
WordCount pgtype.Int4 `json:"word_count"`
|
||||
SourceLabel pgtype.Text `json:"source_label"`
|
||||
TemplateName pgtype.Text `json:"template_name"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListArticles(ctx context.Context, arg ListArticlesParams) ([]ListArticlesRow, error) {
|
||||
rows, err := q.db.Query(ctx, listArticles,
|
||||
arg.TenantID,
|
||||
arg.GenerateStatus,
|
||||
arg.PublishStatus,
|
||||
arg.SourceType,
|
||||
arg.TemplateID,
|
||||
arg.Keyword,
|
||||
arg.PageOffset,
|
||||
arg.PageSize,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListArticlesRow
|
||||
for rows.Next() {
|
||||
var i ListArticlesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.SourceType,
|
||||
&i.TemplateID,
|
||||
&i.GenerateStatus,
|
||||
&i.PublishStatus,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Title,
|
||||
&i.WordCount,
|
||||
&i.SourceLabel,
|
||||
&i.TemplateName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const softDeleteArticle = `-- name: SoftDeleteArticle :exec
|
||||
UPDATE articles SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type SoftDeleteArticleParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) SoftDeleteArticle(ctx context.Context, arg SoftDeleteArticleParams) error {
|
||||
_, err := q.db.Exec(ctx, softDeleteArticle, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateArticleCurrentVersion = `-- name: UpdateArticleCurrentVersion :exec
|
||||
UPDATE articles SET current_version_id = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
`
|
||||
|
||||
type UpdateArticleCurrentVersionParams struct {
|
||||
VersionID pgtype.Int8 `json:"version_id"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateArticleCurrentVersion(ctx context.Context, arg UpdateArticleCurrentVersionParams) error {
|
||||
_, err := q.db.Exec(ctx, updateArticleCurrentVersion, arg.VersionID, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateArticleGenerateStatus = `-- name: UpdateArticleGenerateStatus :exec
|
||||
UPDATE articles SET generate_status = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
`
|
||||
|
||||
type UpdateArticleGenerateStatusParams struct {
|
||||
Status string `json:"status"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateArticleGenerateStatus(ctx context.Context, arg UpdateArticleGenerateStatusParams) error {
|
||||
_, err := q.db.Exec(ctx, updateArticleGenerateStatus, arg.Status, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: audit.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createAuditLog = `-- name: CreateAuditLog :exec
|
||||
INSERT INTO audit_logs (operator_id, tenant_id, module, action, before_json, after_json, result)
|
||||
VALUES ($1, $2, $3, $4,
|
||||
$5, $6, $7)
|
||||
`
|
||||
|
||||
type CreateAuditLogParams struct {
|
||||
OperatorID int64 `json:"operator_id"`
|
||||
TenantID pgtype.Int8 `json:"tenant_id"`
|
||||
Module string `json:"module"`
|
||||
Action string `json:"action"`
|
||||
BeforeJson []byte `json:"before_json"`
|
||||
AfterJson []byte `json:"after_json"`
|
||||
Result pgtype.Text `json:"result"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateAuditLog(ctx context.Context, arg CreateAuditLogParams) error {
|
||||
_, err := q.db.Exec(ctx, createAuditLog,
|
||||
arg.OperatorID,
|
||||
arg.TenantID,
|
||||
arg.Module,
|
||||
arg.Action,
|
||||
arg.BeforeJson,
|
||||
arg.AfterJson,
|
||||
arg.Result,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const createGenerationTask = `-- name: CreateGenerationTask :one
|
||||
INSERT INTO generation_tasks (tenant_id, article_id, quota_reservation_id, task_type, input_params_json, status)
|
||||
VALUES ($1, $2, $3,
|
||||
$4, $5, 'queued')
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type CreateGenerationTaskParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
ArticleID pgtype.Int8 `json:"article_id"`
|
||||
QuotaReservationID pgtype.Int8 `json:"quota_reservation_id"`
|
||||
TaskType string `json:"task_type"`
|
||||
InputParamsJson []byte `json:"input_params_json"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateGenerationTask(ctx context.Context, arg CreateGenerationTaskParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, createGenerationTask,
|
||||
arg.TenantID,
|
||||
arg.ArticleID,
|
||||
arg.QuotaReservationID,
|
||||
arg.TaskType,
|
||||
arg.InputParamsJson,
|
||||
)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const updateGenerationTaskStatus = `-- name: UpdateGenerationTaskStatus :exec
|
||||
UPDATE generation_tasks SET status = $1, error_message = $2,
|
||||
started_at = $3, completed_at = $4, updated_at = NOW()
|
||||
WHERE id = $5 AND tenant_id = $6
|
||||
`
|
||||
|
||||
type UpdateGenerationTaskStatusParams struct {
|
||||
Status string `json:"status"`
|
||||
ErrorMessage pgtype.Text `json:"error_message"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateGenerationTaskStatus(ctx context.Context, arg UpdateGenerationTaskStatusParams) error {
|
||||
_, err := q.db.Exec(ctx, updateGenerationTaskStatus,
|
||||
arg.Status,
|
||||
arg.ErrorMessage,
|
||||
arg.StartedAt,
|
||||
arg.CompletedAt,
|
||||
arg.ID,
|
||||
arg.TenantID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: auth.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const getTenantMembership = `-- name: GetTenantMembership :one
|
||||
SELECT id, tenant_id, user_id, tenant_role, created_at
|
||||
FROM tenant_memberships
|
||||
WHERE user_id = $1 AND deleted_at IS NULL
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetTenantMembershipRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
TenantRole string `json:"tenant_role"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetTenantMembership(ctx context.Context, userID int64) (GetTenantMembershipRow, error) {
|
||||
row := q.db.QueryRow(ctx, getTenantMembership, userID)
|
||||
var i GetTenantMembershipRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.UserID,
|
||||
&i.TenantRole,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getTenantMembershipByTenantAndUser = `-- name: GetTenantMembershipByTenantAndUser :one
|
||||
SELECT id, tenant_id, user_id, tenant_role, created_at
|
||||
FROM tenant_memberships
|
||||
WHERE tenant_id = $1 AND user_id = $2 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type GetTenantMembershipByTenantAndUserParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
type GetTenantMembershipByTenantAndUserRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
TenantRole string `json:"tenant_role"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetTenantMembershipByTenantAndUser(ctx context.Context, arg GetTenantMembershipByTenantAndUserParams) (GetTenantMembershipByTenantAndUserRow, error) {
|
||||
row := q.db.QueryRow(ctx, getTenantMembershipByTenantAndUser, arg.TenantID, arg.UserID)
|
||||
var i GetTenantMembershipByTenantAndUserRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.UserID,
|
||||
&i.TenantRole,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT id, email, phone, password_hash, name, avatar_url, status, created_at, updated_at
|
||||
FROM users
|
||||
WHERE email = $1 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type GetUserByEmailRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Phone pgtype.Text `json:"phone"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
Name string `json:"name"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error) {
|
||||
row := q.db.QueryRow(ctx, getUserByEmail, email)
|
||||
var i GetUserByEmailRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.Phone,
|
||||
&i.PasswordHash,
|
||||
&i.Name,
|
||||
&i.AvatarUrl,
|
||||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, email, phone, password_hash, name, avatar_url, status, created_at, updated_at
|
||||
FROM users
|
||||
WHERE id = $1 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type GetUserByIDRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Phone pgtype.Text `json:"phone"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
Name string `json:"name"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, error) {
|
||||
row := q.db.QueryRow(ctx, getUserByID, id)
|
||||
var i GetUserByIDRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.Phone,
|
||||
&i.PasswordHash,
|
||||
&i.Name,
|
||||
&i.AvatarUrl,
|
||||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -0,0 +1,642 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: brand.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createBrand = `-- name: CreateBrand :one
|
||||
INSERT INTO brands (tenant_id, name, description, status)
|
||||
VALUES ($1, $2, $3, 'active')
|
||||
RETURNING id, created_at
|
||||
`
|
||||
|
||||
type CreateBrandParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
}
|
||||
|
||||
type CreateBrandRow struct {
|
||||
ID int64 `json:"id"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateBrand(ctx context.Context, arg CreateBrandParams) (CreateBrandRow, error) {
|
||||
row := q.db.QueryRow(ctx, createBrand, arg.TenantID, arg.Name, arg.Description)
|
||||
var i CreateBrandRow
|
||||
err := row.Scan(&i.ID, &i.CreatedAt)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createCompetitor = `-- name: CreateCompetitor :one
|
||||
INSERT INTO competitors (tenant_id, brand_id, name, website, description, product_lines_json)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, created_at
|
||||
`
|
||||
|
||||
type CreateCompetitorParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
Name string `json:"name"`
|
||||
Website pgtype.Text `json:"website"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
ProductLinesJson []byte `json:"product_lines_json"`
|
||||
}
|
||||
|
||||
type CreateCompetitorRow struct {
|
||||
ID int64 `json:"id"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateCompetitor(ctx context.Context, arg CreateCompetitorParams) (CreateCompetitorRow, error) {
|
||||
row := q.db.QueryRow(ctx, createCompetitor,
|
||||
arg.TenantID,
|
||||
arg.BrandID,
|
||||
arg.Name,
|
||||
arg.Website,
|
||||
arg.Description,
|
||||
arg.ProductLinesJson,
|
||||
)
|
||||
var i CreateCompetitorRow
|
||||
err := row.Scan(&i.ID, &i.CreatedAt)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createKeyword = `-- name: CreateKeyword :one
|
||||
INSERT INTO brand_keywords (tenant_id, brand_id, name, status)
|
||||
VALUES ($1, $2, $3, 'active')
|
||||
RETURNING id, created_at
|
||||
`
|
||||
|
||||
type CreateKeywordParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type CreateKeywordRow struct {
|
||||
ID int64 `json:"id"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateKeyword(ctx context.Context, arg CreateKeywordParams) (CreateKeywordRow, error) {
|
||||
row := q.db.QueryRow(ctx, createKeyword, arg.TenantID, arg.BrandID, arg.Name)
|
||||
var i CreateKeywordRow
|
||||
err := row.Scan(&i.ID, &i.CreatedAt)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createQuestion = `-- name: CreateQuestion :one
|
||||
INSERT INTO brand_questions (tenant_id, brand_id, keyword_id, status)
|
||||
VALUES ($1, $2, $3, 'active')
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type CreateQuestionParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
KeywordID int64 `json:"keyword_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, createQuestion, arg.TenantID, arg.BrandID, arg.KeywordID)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const createQuestionVersion = `-- name: CreateQuestionVersion :one
|
||||
INSERT INTO brand_question_versions (question_id, question_text, question_hash, version_no, is_active)
|
||||
VALUES ($1, $2, $3, $4, true)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type CreateQuestionVersionParams struct {
|
||||
QuestionID int64 `json:"question_id"`
|
||||
QuestionText string `json:"question_text"`
|
||||
QuestionHash string `json:"question_hash"`
|
||||
VersionNo int32 `json:"version_no"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateQuestionVersion(ctx context.Context, arg CreateQuestionVersionParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, createQuestionVersion,
|
||||
arg.QuestionID,
|
||||
arg.QuestionText,
|
||||
arg.QuestionHash,
|
||||
arg.VersionNo,
|
||||
)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const deactivateQuestionVersion = `-- name: DeactivateQuestionVersion :exec
|
||||
UPDATE brand_question_versions SET is_active = false
|
||||
WHERE question_id = $1 AND is_active = true
|
||||
`
|
||||
|
||||
func (q *Queries) DeactivateQuestionVersion(ctx context.Context, questionID int64) error {
|
||||
_, err := q.db.Exec(ctx, deactivateQuestionVersion, questionID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getBrandByID = `-- name: GetBrandByID :one
|
||||
SELECT id, tenant_id, name, description, status, created_at, updated_at
|
||||
FROM brands
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type GetBrandByIDParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
type GetBrandByIDRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetBrandByID(ctx context.Context, arg GetBrandByIDParams) (GetBrandByIDRow, error) {
|
||||
row := q.db.QueryRow(ctx, getBrandByID, arg.ID, arg.TenantID)
|
||||
var i GetBrandByIDRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getLatestQuestionVersionNo = `-- name: GetLatestQuestionVersionNo :one
|
||||
SELECT COALESCE(MAX(version_no), 0)::INT AS max_version
|
||||
FROM brand_question_versions
|
||||
WHERE question_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetLatestQuestionVersionNo(ctx context.Context, questionID int64) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, getLatestQuestionVersionNo, questionID)
|
||||
var max_version int32
|
||||
err := row.Scan(&max_version)
|
||||
return max_version, err
|
||||
}
|
||||
|
||||
const listBrands = `-- name: ListBrands :many
|
||||
SELECT id, tenant_id, name, description, status, created_at, updated_at
|
||||
FROM brands
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
type ListBrandsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListBrands(ctx context.Context, tenantID int64) ([]ListBrandsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listBrands, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListBrandsRow
|
||||
for rows.Next() {
|
||||
var i ListBrandsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listCompetitors = `-- name: ListCompetitors :many
|
||||
SELECT id, tenant_id, brand_id, name, website, description, product_lines_json, created_at, updated_at
|
||||
FROM competitors
|
||||
WHERE brand_id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
type ListCompetitorsParams struct {
|
||||
BrandID int64 `json:"brand_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
type ListCompetitorsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
Name string `json:"name"`
|
||||
Website pgtype.Text `json:"website"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
ProductLinesJson []byte `json:"product_lines_json"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListCompetitors(ctx context.Context, arg ListCompetitorsParams) ([]ListCompetitorsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listCompetitors, arg.BrandID, arg.TenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListCompetitorsRow
|
||||
for rows.Next() {
|
||||
var i ListCompetitorsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.BrandID,
|
||||
&i.Name,
|
||||
&i.Website,
|
||||
&i.Description,
|
||||
&i.ProductLinesJson,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listKeywords = `-- name: ListKeywords :many
|
||||
SELECT id, tenant_id, brand_id, name, status, created_at, updated_at
|
||||
FROM brand_keywords
|
||||
WHERE brand_id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
type ListKeywordsParams struct {
|
||||
BrandID int64 `json:"brand_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
type ListKeywordsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListKeywords(ctx context.Context, arg ListKeywordsParams) ([]ListKeywordsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listKeywords, arg.BrandID, arg.TenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListKeywordsRow
|
||||
for rows.Next() {
|
||||
var i ListKeywordsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.BrandID,
|
||||
&i.Name,
|
||||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listQuestionVersions = `-- name: ListQuestionVersions :many
|
||||
SELECT v.id, v.question_id, v.question_text, v.question_hash, v.version_no, v.is_active, v.created_at
|
||||
FROM brand_question_versions v
|
||||
JOIN brand_questions q ON q.id = v.question_id
|
||||
WHERE q.brand_id = $1 AND q.tenant_id = $2
|
||||
ORDER BY v.created_at DESC
|
||||
`
|
||||
|
||||
type ListQuestionVersionsParams struct {
|
||||
BrandID int64 `json:"brand_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListQuestionVersions(ctx context.Context, arg ListQuestionVersionsParams) ([]BrandQuestionVersion, error) {
|
||||
rows, err := q.db.Query(ctx, listQuestionVersions, arg.BrandID, arg.TenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []BrandQuestionVersion
|
||||
for rows.Next() {
|
||||
var i BrandQuestionVersion
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.QuestionID,
|
||||
&i.QuestionText,
|
||||
&i.QuestionHash,
|
||||
&i.VersionNo,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listQuestions = `-- name: ListQuestions :many
|
||||
SELECT q.id, q.tenant_id, q.brand_id, q.keyword_id, q.current_version_id, q.status, q.created_at,
|
||||
v.question_text, v.version_no
|
||||
FROM brand_questions q
|
||||
LEFT JOIN brand_question_versions v ON v.id = q.current_version_id
|
||||
WHERE q.brand_id = $1 AND q.tenant_id = $2 AND q.deleted_at IS NULL
|
||||
AND ($3::bigint IS NULL OR q.keyword_id = $3)
|
||||
ORDER BY q.created_at DESC
|
||||
`
|
||||
|
||||
type ListQuestionsParams struct {
|
||||
BrandID int64 `json:"brand_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
KeywordID pgtype.Int8 `json:"keyword_id"`
|
||||
}
|
||||
|
||||
type ListQuestionsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
KeywordID int64 `json:"keyword_id"`
|
||||
CurrentVersionID pgtype.Int8 `json:"current_version_id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
QuestionText pgtype.Text `json:"question_text"`
|
||||
VersionNo pgtype.Int4 `json:"version_no"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]ListQuestionsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listQuestions, arg.BrandID, arg.TenantID, arg.KeywordID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListQuestionsRow
|
||||
for rows.Next() {
|
||||
var i ListQuestionsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.BrandID,
|
||||
&i.KeywordID,
|
||||
&i.CurrentVersionID,
|
||||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.QuestionText,
|
||||
&i.VersionNo,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const softDeleteBrand = `-- name: SoftDeleteBrand :exec
|
||||
UPDATE brands SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type SoftDeleteBrandParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) SoftDeleteBrand(ctx context.Context, arg SoftDeleteBrandParams) error {
|
||||
_, err := q.db.Exec(ctx, softDeleteBrand, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const softDeleteCompetitor = `-- name: SoftDeleteCompetitor :exec
|
||||
UPDATE competitors SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1 AND brand_id = $2 AND tenant_id = $3 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type SoftDeleteCompetitorParams struct {
|
||||
ID int64 `json:"id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) SoftDeleteCompetitor(ctx context.Context, arg SoftDeleteCompetitorParams) error {
|
||||
_, err := q.db.Exec(ctx, softDeleteCompetitor, arg.ID, arg.BrandID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const softDeleteCompetitorsByBrand = `-- name: SoftDeleteCompetitorsByBrand :exec
|
||||
UPDATE competitors SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE brand_id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type SoftDeleteCompetitorsByBrandParams struct {
|
||||
BrandID int64 `json:"brand_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) SoftDeleteCompetitorsByBrand(ctx context.Context, arg SoftDeleteCompetitorsByBrandParams) error {
|
||||
_, err := q.db.Exec(ctx, softDeleteCompetitorsByBrand, arg.BrandID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const softDeleteKeyword = `-- name: SoftDeleteKeyword :exec
|
||||
UPDATE brand_keywords SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1 AND brand_id = $2 AND tenant_id = $3 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type SoftDeleteKeywordParams struct {
|
||||
ID int64 `json:"id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) SoftDeleteKeyword(ctx context.Context, arg SoftDeleteKeywordParams) error {
|
||||
_, err := q.db.Exec(ctx, softDeleteKeyword, arg.ID, arg.BrandID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const softDeleteKeywordsByBrand = `-- name: SoftDeleteKeywordsByBrand :exec
|
||||
UPDATE brand_keywords SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE brand_id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type SoftDeleteKeywordsByBrandParams struct {
|
||||
BrandID int64 `json:"brand_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) SoftDeleteKeywordsByBrand(ctx context.Context, arg SoftDeleteKeywordsByBrandParams) error {
|
||||
_, err := q.db.Exec(ctx, softDeleteKeywordsByBrand, arg.BrandID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const softDeleteQuestion = `-- name: SoftDeleteQuestion :exec
|
||||
UPDATE brand_questions SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1 AND brand_id = $2 AND tenant_id = $3 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type SoftDeleteQuestionParams struct {
|
||||
ID int64 `json:"id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) SoftDeleteQuestion(ctx context.Context, arg SoftDeleteQuestionParams) error {
|
||||
_, err := q.db.Exec(ctx, softDeleteQuestion, arg.ID, arg.BrandID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const softDeleteQuestionsByBrand = `-- name: SoftDeleteQuestionsByBrand :exec
|
||||
UPDATE brand_questions SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE brand_id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type SoftDeleteQuestionsByBrandParams struct {
|
||||
BrandID int64 `json:"brand_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) SoftDeleteQuestionsByBrand(ctx context.Context, arg SoftDeleteQuestionsByBrandParams) error {
|
||||
_, err := q.db.Exec(ctx, softDeleteQuestionsByBrand, arg.BrandID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateBrand = `-- name: UpdateBrand :exec
|
||||
UPDATE brands SET name = $1, description = $2, updated_at = NOW()
|
||||
WHERE id = $3 AND tenant_id = $4 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type UpdateBrandParams struct {
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateBrand(ctx context.Context, arg UpdateBrandParams) error {
|
||||
_, err := q.db.Exec(ctx, updateBrand,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.ID,
|
||||
arg.TenantID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateCompetitor = `-- name: UpdateCompetitor :exec
|
||||
UPDATE competitors SET name = $1, website = $2,
|
||||
description = $3, product_lines_json = $4, updated_at = NOW()
|
||||
WHERE id = $5 AND brand_id = $6 AND tenant_id = $7 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type UpdateCompetitorParams struct {
|
||||
Name string `json:"name"`
|
||||
Website pgtype.Text `json:"website"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
ProductLinesJson []byte `json:"product_lines_json"`
|
||||
ID int64 `json:"id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateCompetitor(ctx context.Context, arg UpdateCompetitorParams) error {
|
||||
_, err := q.db.Exec(ctx, updateCompetitor,
|
||||
arg.Name,
|
||||
arg.Website,
|
||||
arg.Description,
|
||||
arg.ProductLinesJson,
|
||||
arg.ID,
|
||||
arg.BrandID,
|
||||
arg.TenantID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateKeyword = `-- name: UpdateKeyword :exec
|
||||
UPDATE brand_keywords SET name = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND brand_id = $3 AND tenant_id = $4 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type UpdateKeywordParams struct {
|
||||
Name string `json:"name"`
|
||||
ID int64 `json:"id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateKeyword(ctx context.Context, arg UpdateKeywordParams) error {
|
||||
_, err := q.db.Exec(ctx, updateKeyword,
|
||||
arg.Name,
|
||||
arg.ID,
|
||||
arg.BrandID,
|
||||
arg.TenantID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateQuestionCurrentVersion = `-- name: UpdateQuestionCurrentVersion :exec
|
||||
UPDATE brand_questions SET current_version_id = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
`
|
||||
|
||||
type UpdateQuestionCurrentVersionParams struct {
|
||||
VersionID pgtype.Int8 `json:"version_id"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateQuestionCurrentVersion(ctx context.Context, arg UpdateQuestionCurrentVersionParams) error {
|
||||
_, err := q.db.Exec(ctx, updateQuestionCurrentVersion, arg.VersionID, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type Article struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
SourceType string `json:"source_type"`
|
||||
TemplateID pgtype.Int8 `json:"template_id"`
|
||||
PromptRuleID pgtype.Int8 `json:"prompt_rule_id"`
|
||||
CurrentVersionID pgtype.Int8 `json:"current_version_id"`
|
||||
GenerateStatus string `json:"generate_status"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type ArticleTemplate struct {
|
||||
ID int64 `json:"id"`
|
||||
Scope string `json:"scope"`
|
||||
TenantID pgtype.Int8 `json:"tenant_id"`
|
||||
OriginType string `json:"origin_type"`
|
||||
ShareCodeID pgtype.Int8 `json:"share_code_id"`
|
||||
TemplateKey string `json:"template_key"`
|
||||
TemplateName string `json:"template_name"`
|
||||
SchemaJson []byte `json:"schema_json"`
|
||||
PromptTemplate pgtype.Text `json:"prompt_template"`
|
||||
PromptVisibility string `json:"prompt_visibility"`
|
||||
ProtectedPromptAssetKey pgtype.Text `json:"protected_prompt_asset_key"`
|
||||
CardConfigJson []byte `json:"card_config_json"`
|
||||
Status string `json:"status"`
|
||||
VersionNo int32 `json:"version_no"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type ArticleVersion struct {
|
||||
ID int64 `json:"id"`
|
||||
ArticleID int64 `json:"article_id"`
|
||||
VersionNo int32 `json:"version_no"`
|
||||
Title pgtype.Text `json:"title"`
|
||||
HtmlContent pgtype.Text `json:"html_content"`
|
||||
MarkdownContent pgtype.Text `json:"markdown_content"`
|
||||
WordCount int32 `json:"word_count"`
|
||||
SourceLabel pgtype.Text `json:"source_label"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type AuditLog struct {
|
||||
ID int64 `json:"id"`
|
||||
OperatorID int64 `json:"operator_id"`
|
||||
TenantID pgtype.Int8 `json:"tenant_id"`
|
||||
Module string `json:"module"`
|
||||
Action string `json:"action"`
|
||||
BeforeJson []byte `json:"before_json"`
|
||||
AfterJson []byte `json:"after_json"`
|
||||
Result pgtype.Text `json:"result"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Brand struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type BrandKeyword struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type BrandQuestion struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
KeywordID int64 `json:"keyword_id"`
|
||||
CurrentVersionID pgtype.Int8 `json:"current_version_id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type BrandQuestionVersion struct {
|
||||
ID int64 `json:"id"`
|
||||
QuestionID int64 `json:"question_id"`
|
||||
QuestionText string `json:"question_text"`
|
||||
QuestionHash string `json:"question_hash"`
|
||||
VersionNo int32 `json:"version_no"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Competitor struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
BrandID int64 `json:"brand_id"`
|
||||
Name string `json:"name"`
|
||||
Website pgtype.Text `json:"website"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
ProductLinesJson []byte `json:"product_lines_json"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type GenerationTask struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
ArticleID pgtype.Int8 `json:"article_id"`
|
||||
TaskBatchID pgtype.Text `json:"task_batch_id"`
|
||||
QuotaReservationID pgtype.Int8 `json:"quota_reservation_id"`
|
||||
TaskType string `json:"task_type"`
|
||||
RequestHash pgtype.Text `json:"request_hash"`
|
||||
InputParamsJson []byte `json:"input_params_json"`
|
||||
Status string `json:"status"`
|
||||
ErrorMessage pgtype.Text `json:"error_message"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Plan struct {
|
||||
ID int64 `json:"id"`
|
||||
PlanCode string `json:"plan_code"`
|
||||
Name string `json:"name"`
|
||||
QuotaPolicyJson []byte `json:"quota_policy_json"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type PlatformUserRole struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
PlatformRole string `json:"platform_role"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type QuotaReservation struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
QuotaType string `json:"quota_type"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
ReservedAmount int32 `json:"reserved_amount"`
|
||||
ConsumedAmount int32 `json:"consumed_amount"`
|
||||
RefundedAmount int32 `json:"refunded_amount"`
|
||||
Status string `json:"status"`
|
||||
ExpireAt pgtype.Timestamptz `json:"expire_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type TaskRecord struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
TaskType string `json:"task_type"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
Status string `json:"status"`
|
||||
RetryCount int32 `json:"retry_count"`
|
||||
TaskBatchID pgtype.Text `json:"task_batch_id"`
|
||||
ErrorMessage pgtype.Text `json:"error_message"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Tenant struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
FrozenAt pgtype.Timestamptz `json:"frozen_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type TenantMembership struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
TenantRole string `json:"tenant_role"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type TenantPlanSubscription struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
PlanID int64 `json:"plan_id"`
|
||||
StartAt pgtype.Timestamptz `json:"start_at"`
|
||||
EndAt pgtype.Timestamptz `json:"end_at"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type TenantQuotaLedger struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
QuotaType string `json:"quota_type"`
|
||||
Delta int32 `json:"delta"`
|
||||
BalanceAfter int32 `json:"balance_after"`
|
||||
Reason pgtype.Text `json:"reason"`
|
||||
ReferenceType pgtype.Text `json:"reference_type"`
|
||||
ReferenceID pgtype.Int8 `json:"reference_id"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Phone pgtype.Text `json:"phone"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
Name string `json:"name"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Querier interface {
|
||||
ConfirmReservation(ctx context.Context, arg ConfirmReservationParams) error
|
||||
CountArticles(ctx context.Context, arg CountArticlesParams) (int64, error)
|
||||
CountArticlesByTenant(ctx context.Context, tenantID int64) (int64, error)
|
||||
CountBrandsByTenant(ctx context.Context, tenantID int64) (int64, error)
|
||||
CountPublishedArticlesByTenant(ctx context.Context, tenantID int64) (int64, error)
|
||||
CreateArticle(ctx context.Context, arg CreateArticleParams) (int64, error)
|
||||
CreateArticleVersion(ctx context.Context, arg CreateArticleVersionParams) (int64, error)
|
||||
CreateAuditLog(ctx context.Context, arg CreateAuditLogParams) error
|
||||
CreateBrand(ctx context.Context, arg CreateBrandParams) (CreateBrandRow, error)
|
||||
CreateCompetitor(ctx context.Context, arg CreateCompetitorParams) (CreateCompetitorRow, error)
|
||||
CreateGenerationTask(ctx context.Context, arg CreateGenerationTaskParams) (int64, error)
|
||||
CreateKeyword(ctx context.Context, arg CreateKeywordParams) (CreateKeywordRow, error)
|
||||
CreateQuestion(ctx context.Context, arg CreateQuestionParams) (int64, error)
|
||||
CreateQuestionVersion(ctx context.Context, arg CreateQuestionVersionParams) (int64, error)
|
||||
CreateQuotaReservation(ctx context.Context, arg CreateQuotaReservationParams) (int64, error)
|
||||
DeactivateQuestionVersion(ctx context.Context, questionID int64) error
|
||||
GetActivePlanForTenant(ctx context.Context, tenantID int64) (GetActivePlanForTenantRow, error)
|
||||
GetArticleByID(ctx context.Context, arg GetArticleByIDParams) (GetArticleByIDRow, error)
|
||||
GetArticleVersions(ctx context.Context, articleID int64) ([]GetArticleVersionsRow, error)
|
||||
GetBrandByID(ctx context.Context, arg GetBrandByIDParams) (GetBrandByIDRow, error)
|
||||
GetCurrentBalance(ctx context.Context, arg GetCurrentBalanceParams) (int32, error)
|
||||
GetLatestQuestionVersionNo(ctx context.Context, questionID int64) (int32, error)
|
||||
GetQuotaSummary(ctx context.Context, tenantID int64) (int32, error)
|
||||
GetRecentArticles(ctx context.Context, tenantID int64) ([]GetRecentArticlesRow, error)
|
||||
GetTemplateByID(ctx context.Context, arg GetTemplateByIDParams) (GetTemplateByIDRow, error)
|
||||
GetTemplateSchema(ctx context.Context, id int64) (GetTemplateSchemaRow, error)
|
||||
GetTenantMembership(ctx context.Context, userID int64) (GetTenantMembershipRow, error)
|
||||
GetTenantMembershipByTenantAndUser(ctx context.Context, arg GetTenantMembershipByTenantAndUserParams) (GetTenantMembershipByTenantAndUserRow, error)
|
||||
GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error)
|
||||
GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, error)
|
||||
InsertQuotaLedger(ctx context.Context, arg InsertQuotaLedgerParams) (int64, error)
|
||||
ListArticles(ctx context.Context, arg ListArticlesParams) ([]ListArticlesRow, error)
|
||||
ListBrands(ctx context.Context, tenantID int64) ([]ListBrandsRow, error)
|
||||
ListCompetitors(ctx context.Context, arg ListCompetitorsParams) ([]ListCompetitorsRow, error)
|
||||
ListKeywords(ctx context.Context, arg ListKeywordsParams) ([]ListKeywordsRow, error)
|
||||
ListQuestionVersions(ctx context.Context, arg ListQuestionVersionsParams) ([]BrandQuestionVersion, error)
|
||||
ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]ListQuestionsRow, error)
|
||||
ListTemplates(ctx context.Context, tenantID int64) ([]ListTemplatesRow, error)
|
||||
RefundReservation(ctx context.Context, arg RefundReservationParams) error
|
||||
SoftDeleteArticle(ctx context.Context, arg SoftDeleteArticleParams) error
|
||||
SoftDeleteBrand(ctx context.Context, arg SoftDeleteBrandParams) error
|
||||
SoftDeleteCompetitor(ctx context.Context, arg SoftDeleteCompetitorParams) error
|
||||
SoftDeleteCompetitorsByBrand(ctx context.Context, arg SoftDeleteCompetitorsByBrandParams) error
|
||||
SoftDeleteKeyword(ctx context.Context, arg SoftDeleteKeywordParams) error
|
||||
SoftDeleteKeywordsByBrand(ctx context.Context, arg SoftDeleteKeywordsByBrandParams) error
|
||||
SoftDeleteQuestion(ctx context.Context, arg SoftDeleteQuestionParams) error
|
||||
SoftDeleteQuestionsByBrand(ctx context.Context, arg SoftDeleteQuestionsByBrandParams) error
|
||||
UpdateArticleCurrentVersion(ctx context.Context, arg UpdateArticleCurrentVersionParams) error
|
||||
UpdateArticleGenerateStatus(ctx context.Context, arg UpdateArticleGenerateStatusParams) error
|
||||
UpdateBrand(ctx context.Context, arg UpdateBrandParams) error
|
||||
UpdateCompetitor(ctx context.Context, arg UpdateCompetitorParams) error
|
||||
UpdateGenerationTaskStatus(ctx context.Context, arg UpdateGenerationTaskStatusParams) error
|
||||
UpdateKeyword(ctx context.Context, arg UpdateKeywordParams) error
|
||||
UpdateQuestionCurrentVersion(ctx context.Context, arg UpdateQuestionCurrentVersionParams) error
|
||||
UpdateQuotaReservationResource(ctx context.Context, arg UpdateQuotaReservationResourceParams) error
|
||||
}
|
||||
|
||||
var _ Querier = (*Queries)(nil)
|
||||
@@ -0,0 +1,140 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: quota.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const confirmReservation = `-- name: ConfirmReservation :exec
|
||||
UPDATE quota_reservations SET status = 'confirmed', consumed_amount = reserved_amount, updated_at = NOW()
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
`
|
||||
|
||||
type ConfirmReservationParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) ConfirmReservation(ctx context.Context, arg ConfirmReservationParams) error {
|
||||
_, err := q.db.Exec(ctx, confirmReservation, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const createQuotaReservation = `-- name: CreateQuotaReservation :one
|
||||
INSERT INTO quota_reservations (tenant_id, quota_type, resource_type, resource_id, reserved_amount, status, expire_at)
|
||||
VALUES ($1, $2, $3, $4,
|
||||
$5, 'pending', $6)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type CreateQuotaReservationParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
QuotaType string `json:"quota_type"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
ReservedAmount int32 `json:"reserved_amount"`
|
||||
ExpireAt pgtype.Timestamptz `json:"expire_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateQuotaReservation(ctx context.Context, arg CreateQuotaReservationParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, createQuotaReservation,
|
||||
arg.TenantID,
|
||||
arg.QuotaType,
|
||||
arg.ResourceType,
|
||||
arg.ResourceID,
|
||||
arg.ReservedAmount,
|
||||
arg.ExpireAt,
|
||||
)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const getCurrentBalance = `-- name: GetCurrentBalance :one
|
||||
SELECT COALESCE(
|
||||
(SELECT balance_after FROM tenant_quota_ledgers
|
||||
WHERE tenant_id = $1 AND quota_type = $2
|
||||
ORDER BY created_at DESC LIMIT 1), 0
|
||||
)::INT AS balance
|
||||
`
|
||||
|
||||
type GetCurrentBalanceParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
QuotaType string `json:"quota_type"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetCurrentBalance(ctx context.Context, arg GetCurrentBalanceParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, getCurrentBalance, arg.TenantID, arg.QuotaType)
|
||||
var balance int32
|
||||
err := row.Scan(&balance)
|
||||
return balance, err
|
||||
}
|
||||
|
||||
const insertQuotaLedger = `-- name: InsertQuotaLedger :one
|
||||
INSERT INTO tenant_quota_ledgers (tenant_id, quota_type, delta, balance_after, reason, reference_type, reference_id)
|
||||
VALUES ($1, $2, $3, $4,
|
||||
$5, $6, $7)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type InsertQuotaLedgerParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
QuotaType string `json:"quota_type"`
|
||||
Delta int32 `json:"delta"`
|
||||
BalanceAfter int32 `json:"balance_after"`
|
||||
Reason pgtype.Text `json:"reason"`
|
||||
ReferenceType pgtype.Text `json:"reference_type"`
|
||||
ReferenceID pgtype.Int8 `json:"reference_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) InsertQuotaLedger(ctx context.Context, arg InsertQuotaLedgerParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, insertQuotaLedger,
|
||||
arg.TenantID,
|
||||
arg.QuotaType,
|
||||
arg.Delta,
|
||||
arg.BalanceAfter,
|
||||
arg.Reason,
|
||||
arg.ReferenceType,
|
||||
arg.ReferenceID,
|
||||
)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const refundReservation = `-- name: RefundReservation :exec
|
||||
UPDATE quota_reservations SET status = 'refunded', refunded_amount = reserved_amount, updated_at = NOW()
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
`
|
||||
|
||||
type RefundReservationParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) RefundReservation(ctx context.Context, arg RefundReservationParams) error {
|
||||
_, err := q.db.Exec(ctx, refundReservation, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateQuotaReservationResource = `-- name: UpdateQuotaReservationResource :exec
|
||||
UPDATE quota_reservations SET resource_id = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
`
|
||||
|
||||
type UpdateQuotaReservationResourceParams struct {
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateQuotaReservationResource(ctx context.Context, arg UpdateQuotaReservationResourceParams) error {
|
||||
_, err := q.db.Exec(ctx, updateQuotaReservationResource, arg.ResourceID, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: template.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const getTemplateByID = `-- name: GetTemplateByID :one
|
||||
SELECT id, scope, tenant_id, origin_type, template_key, template_name,
|
||||
schema_json, prompt_template, prompt_visibility, protected_prompt_asset_key,
|
||||
card_config_json, status, version_no, created_at, updated_at
|
||||
FROM article_templates
|
||||
WHERE id = $1
|
||||
AND (scope = 'platform' OR tenant_id = $2::bigint)
|
||||
AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type GetTemplateByIDParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
type GetTemplateByIDRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Scope string `json:"scope"`
|
||||
TenantID pgtype.Int8 `json:"tenant_id"`
|
||||
OriginType string `json:"origin_type"`
|
||||
TemplateKey string `json:"template_key"`
|
||||
TemplateName string `json:"template_name"`
|
||||
SchemaJson []byte `json:"schema_json"`
|
||||
PromptTemplate pgtype.Text `json:"prompt_template"`
|
||||
PromptVisibility string `json:"prompt_visibility"`
|
||||
ProtectedPromptAssetKey pgtype.Text `json:"protected_prompt_asset_key"`
|
||||
CardConfigJson []byte `json:"card_config_json"`
|
||||
Status string `json:"status"`
|
||||
VersionNo int32 `json:"version_no"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetTemplateByID(ctx context.Context, arg GetTemplateByIDParams) (GetTemplateByIDRow, error) {
|
||||
row := q.db.QueryRow(ctx, getTemplateByID, arg.ID, arg.TenantID)
|
||||
var i GetTemplateByIDRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Scope,
|
||||
&i.TenantID,
|
||||
&i.OriginType,
|
||||
&i.TemplateKey,
|
||||
&i.TemplateName,
|
||||
&i.SchemaJson,
|
||||
&i.PromptTemplate,
|
||||
&i.PromptVisibility,
|
||||
&i.ProtectedPromptAssetKey,
|
||||
&i.CardConfigJson,
|
||||
&i.Status,
|
||||
&i.VersionNo,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getTemplateSchema = `-- name: GetTemplateSchema :one
|
||||
SELECT id, template_name, schema_json
|
||||
FROM article_templates
|
||||
WHERE id = $1 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type GetTemplateSchemaRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TemplateName string `json:"template_name"`
|
||||
SchemaJson []byte `json:"schema_json"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetTemplateSchema(ctx context.Context, id int64) (GetTemplateSchemaRow, error) {
|
||||
row := q.db.QueryRow(ctx, getTemplateSchema, id)
|
||||
var i GetTemplateSchemaRow
|
||||
err := row.Scan(&i.ID, &i.TemplateName, &i.SchemaJson)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listTemplates = `-- name: ListTemplates :many
|
||||
SELECT id, scope, tenant_id, origin_type, template_key, template_name,
|
||||
schema_json, prompt_template, prompt_visibility, card_config_json,
|
||||
status, version_no, created_at, updated_at
|
||||
FROM article_templates
|
||||
WHERE (scope = 'platform' OR tenant_id = $1::bigint)
|
||||
AND status = 'active' AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
type ListTemplatesRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Scope string `json:"scope"`
|
||||
TenantID pgtype.Int8 `json:"tenant_id"`
|
||||
OriginType string `json:"origin_type"`
|
||||
TemplateKey string `json:"template_key"`
|
||||
TemplateName string `json:"template_name"`
|
||||
SchemaJson []byte `json:"schema_json"`
|
||||
PromptTemplate pgtype.Text `json:"prompt_template"`
|
||||
PromptVisibility string `json:"prompt_visibility"`
|
||||
CardConfigJson []byte `json:"card_config_json"`
|
||||
Status string `json:"status"`
|
||||
VersionNo int32 `json:"version_no"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListTemplates(ctx context.Context, tenantID int64) ([]ListTemplatesRow, error) {
|
||||
rows, err := q.db.Query(ctx, listTemplates, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListTemplatesRow
|
||||
for rows.Next() {
|
||||
var i ListTemplatesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Scope,
|
||||
&i.TenantID,
|
||||
&i.OriginType,
|
||||
&i.TemplateKey,
|
||||
&i.TemplateName,
|
||||
&i.SchemaJson,
|
||||
&i.PromptTemplate,
|
||||
&i.PromptVisibility,
|
||||
&i.CardConfigJson,
|
||||
&i.Status,
|
||||
&i.VersionNo,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: workspace.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const countArticlesByTenant = `-- name: CountArticlesByTenant :one
|
||||
SELECT COUNT(*) FROM articles
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
func (q *Queries) CountArticlesByTenant(ctx context.Context, tenantID int64) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countArticlesByTenant, tenantID)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const countBrandsByTenant = `-- name: CountBrandsByTenant :one
|
||||
SELECT COUNT(*) FROM brands
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
func (q *Queries) CountBrandsByTenant(ctx context.Context, tenantID int64) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countBrandsByTenant, tenantID)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const countPublishedArticlesByTenant = `-- name: CountPublishedArticlesByTenant :one
|
||||
SELECT COUNT(*) FROM articles
|
||||
WHERE tenant_id = $1 AND publish_status = 'success' AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
func (q *Queries) CountPublishedArticlesByTenant(ctx context.Context, tenantID int64) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countPublishedArticlesByTenant, tenantID)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const getActivePlanForTenant = `-- name: GetActivePlanForTenant :one
|
||||
SELECT p.plan_code, p.name AS plan_name, p.quota_policy_json,
|
||||
s.start_at, s.end_at
|
||||
FROM tenant_plan_subscriptions s
|
||||
JOIN plans p ON p.id = s.plan_id
|
||||
WHERE s.tenant_id = $1 AND s.status = 'active' AND s.deleted_at IS NULL
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetActivePlanForTenantRow struct {
|
||||
PlanCode string `json:"plan_code"`
|
||||
PlanName string `json:"plan_name"`
|
||||
QuotaPolicyJson []byte `json:"quota_policy_json"`
|
||||
StartAt pgtype.Timestamptz `json:"start_at"`
|
||||
EndAt pgtype.Timestamptz `json:"end_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetActivePlanForTenant(ctx context.Context, tenantID int64) (GetActivePlanForTenantRow, error) {
|
||||
row := q.db.QueryRow(ctx, getActivePlanForTenant, tenantID)
|
||||
var i GetActivePlanForTenantRow
|
||||
err := row.Scan(
|
||||
&i.PlanCode,
|
||||
&i.PlanName,
|
||||
&i.QuotaPolicyJson,
|
||||
&i.StartAt,
|
||||
&i.EndAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getQuotaSummary = `-- name: GetQuotaSummary :one
|
||||
SELECT COALESCE(
|
||||
(SELECT balance_after FROM tenant_quota_ledgers
|
||||
WHERE tenant_id = $1 AND quota_type = 'article_generation'
|
||||
ORDER BY created_at DESC LIMIT 1), 0
|
||||
)::INT AS balance
|
||||
`
|
||||
|
||||
func (q *Queries) GetQuotaSummary(ctx context.Context, tenantID int64) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, getQuotaSummary, tenantID)
|
||||
var balance int32
|
||||
err := row.Scan(&balance)
|
||||
return balance, err
|
||||
}
|
||||
|
||||
const getRecentArticles = `-- name: GetRecentArticles :many
|
||||
SELECT a.id, a.generate_status, a.publish_status, a.source_type, a.created_at,
|
||||
av.title, av.word_count, av.source_label,
|
||||
t.template_name
|
||||
FROM articles a
|
||||
LEFT JOIN article_versions av ON av.id = a.current_version_id
|
||||
LEFT JOIN article_templates t ON t.id = a.template_id
|
||||
WHERE a.tenant_id = $1 AND a.deleted_at IS NULL
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT 10
|
||||
`
|
||||
|
||||
type GetRecentArticlesRow struct {
|
||||
ID int64 `json:"id"`
|
||||
GenerateStatus string `json:"generate_status"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
SourceType string `json:"source_type"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
Title pgtype.Text `json:"title"`
|
||||
WordCount pgtype.Int4 `json:"word_count"`
|
||||
SourceLabel pgtype.Text `json:"source_label"`
|
||||
TemplateName pgtype.Text `json:"template_name"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetRecentArticles(ctx context.Context, tenantID int64) ([]GetRecentArticlesRow, error) {
|
||||
rows, err := q.db.Query(ctx, getRecentArticles, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetRecentArticlesRow
|
||||
for rows.Next() {
|
||||
var i GetRecentArticlesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.GenerateStatus,
|
||||
&i.PublishStatus,
|
||||
&i.SourceType,
|
||||
&i.CreatedAt,
|
||||
&i.Title,
|
||||
&i.WordCount,
|
||||
&i.SourceLabel,
|
||||
&i.TemplateName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
-- name: ListArticles :many
|
||||
SELECT a.id, a.tenant_id, a.source_type, a.template_id,
|
||||
a.generate_status, a.publish_status, a.created_at, a.updated_at,
|
||||
av.title, av.word_count, av.source_label,
|
||||
t.template_name
|
||||
FROM articles a
|
||||
LEFT JOIN article_versions av ON av.id = a.current_version_id
|
||||
LEFT JOIN article_templates t ON t.id = a.template_id
|
||||
WHERE a.tenant_id = sqlc.arg(tenant_id)
|
||||
AND a.deleted_at IS NULL
|
||||
AND (sqlc.narg(generate_status)::text IS NULL OR a.generate_status = sqlc.narg(generate_status))
|
||||
AND (sqlc.narg(publish_status)::text IS NULL OR a.publish_status = sqlc.narg(publish_status))
|
||||
AND (sqlc.narg(source_type)::text IS NULL OR a.source_type = sqlc.narg(source_type))
|
||||
AND (sqlc.narg(template_id)::bigint IS NULL OR a.template_id = sqlc.narg(template_id))
|
||||
AND (sqlc.narg(keyword)::text IS NULL OR av.title ILIKE '%' || sqlc.narg(keyword) || '%')
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT sqlc.arg(page_size) OFFSET sqlc.arg(page_offset);
|
||||
|
||||
-- name: CountArticles :one
|
||||
SELECT COUNT(*)
|
||||
FROM articles a
|
||||
LEFT JOIN article_versions av ON av.id = a.current_version_id
|
||||
WHERE a.tenant_id = sqlc.arg(tenant_id)
|
||||
AND a.deleted_at IS NULL
|
||||
AND (sqlc.narg(generate_status)::text IS NULL OR a.generate_status = sqlc.narg(generate_status))
|
||||
AND (sqlc.narg(publish_status)::text IS NULL OR a.publish_status = sqlc.narg(publish_status))
|
||||
AND (sqlc.narg(source_type)::text IS NULL OR a.source_type = sqlc.narg(source_type))
|
||||
AND (sqlc.narg(template_id)::bigint IS NULL OR a.template_id = sqlc.narg(template_id))
|
||||
AND (sqlc.narg(keyword)::text IS NULL OR av.title ILIKE '%' || sqlc.narg(keyword) || '%');
|
||||
|
||||
-- name: GetArticleByID :one
|
||||
SELECT a.id, a.tenant_id, a.source_type, a.template_id, a.current_version_id,
|
||||
a.generate_status, a.publish_status, a.created_at, a.updated_at,
|
||||
av.title, av.html_content, av.markdown_content, av.word_count, av.source_label, av.version_no,
|
||||
t.template_name
|
||||
FROM articles a
|
||||
LEFT JOIN article_versions av ON av.id = a.current_version_id
|
||||
LEFT JOIN article_templates t ON t.id = a.template_id
|
||||
WHERE a.id = sqlc.arg(id) AND a.tenant_id = sqlc.arg(tenant_id) AND a.deleted_at IS NULL;
|
||||
|
||||
-- name: GetArticleVersions :many
|
||||
SELECT id, article_id, version_no, title, word_count, source_label, created_at
|
||||
FROM article_versions
|
||||
WHERE article_id = sqlc.arg(article_id)
|
||||
ORDER BY version_no DESC;
|
||||
|
||||
-- name: CreateArticle :one
|
||||
INSERT INTO articles (tenant_id, source_type, template_id, generate_status, publish_status)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.arg(source_type), sqlc.arg(template_id), 'pending', 'unpublished')
|
||||
RETURNING id;
|
||||
|
||||
-- name: CreateArticleVersion :one
|
||||
INSERT INTO article_versions (article_id, version_no, title, html_content, markdown_content, word_count, source_label)
|
||||
VALUES (sqlc.arg(article_id), sqlc.arg(version_no), sqlc.arg(title), sqlc.arg(html_content), sqlc.arg(markdown_content), sqlc.arg(word_count), sqlc.arg(source_label))
|
||||
RETURNING id;
|
||||
|
||||
-- name: UpdateArticleCurrentVersion :exec
|
||||
UPDATE articles SET current_version_id = sqlc.arg(version_id), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: UpdateArticleGenerateStatus :exec
|
||||
UPDATE articles SET generate_status = sqlc.arg(status), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: SoftDeleteArticle :exec
|
||||
UPDATE articles SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- name: CreateAuditLog :exec
|
||||
INSERT INTO audit_logs (operator_id, tenant_id, module, action, before_json, after_json, result)
|
||||
VALUES (sqlc.arg(operator_id), sqlc.arg(tenant_id), sqlc.arg(module), sqlc.arg(action),
|
||||
sqlc.arg(before_json), sqlc.arg(after_json), sqlc.arg(result));
|
||||
|
||||
-- name: CreateGenerationTask :one
|
||||
INSERT INTO generation_tasks (tenant_id, article_id, quota_reservation_id, task_type, input_params_json, status)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.arg(article_id), sqlc.arg(quota_reservation_id),
|
||||
sqlc.arg(task_type), sqlc.arg(input_params_json), 'queued')
|
||||
RETURNING id;
|
||||
|
||||
-- name: UpdateGenerationTaskStatus :exec
|
||||
UPDATE generation_tasks SET status = sqlc.arg(status), error_message = sqlc.arg(error_message),
|
||||
started_at = sqlc.arg(started_at), completed_at = sqlc.arg(completed_at), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
@@ -0,0 +1,20 @@
|
||||
-- name: GetUserByEmail :one
|
||||
SELECT id, email, phone, password_hash, name, avatar_url, status, created_at, updated_at
|
||||
FROM users
|
||||
WHERE email = sqlc.arg(email) AND deleted_at IS NULL;
|
||||
|
||||
-- name: GetUserByID :one
|
||||
SELECT id, email, phone, password_hash, name, avatar_url, status, created_at, updated_at
|
||||
FROM users
|
||||
WHERE id = sqlc.arg(id) AND deleted_at IS NULL;
|
||||
|
||||
-- name: GetTenantMembership :one
|
||||
SELECT id, tenant_id, user_id, tenant_role, created_at
|
||||
FROM tenant_memberships
|
||||
WHERE user_id = sqlc.arg(user_id) AND deleted_at IS NULL
|
||||
LIMIT 1;
|
||||
|
||||
-- name: GetTenantMembershipByTenantAndUser :one
|
||||
SELECT id, tenant_id, user_id, tenant_role, created_at
|
||||
FROM tenant_memberships
|
||||
WHERE tenant_id = sqlc.arg(tenant_id) AND user_id = sqlc.arg(user_id) AND deleted_at IS NULL;
|
||||
@@ -0,0 +1,117 @@
|
||||
-- name: ListBrands :many
|
||||
SELECT id, tenant_id, name, description, status, created_at, updated_at
|
||||
FROM brands
|
||||
WHERE tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: GetBrandByID :one
|
||||
SELECT id, tenant_id, name, description, status, created_at, updated_at
|
||||
FROM brands
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
|
||||
-- name: CreateBrand :one
|
||||
INSERT INTO brands (tenant_id, name, description, status)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.arg(name), sqlc.arg(description), 'active')
|
||||
RETURNING id, created_at;
|
||||
|
||||
-- name: UpdateBrand :exec
|
||||
UPDATE brands SET name = sqlc.arg(name), description = sqlc.arg(description), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
|
||||
-- name: SoftDeleteBrand :exec
|
||||
UPDATE brands SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
|
||||
-- name: ListKeywords :many
|
||||
SELECT id, tenant_id, brand_id, name, status, created_at, updated_at
|
||||
FROM brand_keywords
|
||||
WHERE brand_id = sqlc.arg(brand_id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: CreateKeyword :one
|
||||
INSERT INTO brand_keywords (tenant_id, brand_id, name, status)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.arg(brand_id), sqlc.arg(name), 'active')
|
||||
RETURNING id, created_at;
|
||||
|
||||
-- name: UpdateKeyword :exec
|
||||
UPDATE brand_keywords SET name = sqlc.arg(name), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND brand_id = sqlc.arg(brand_id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
|
||||
-- name: SoftDeleteKeyword :exec
|
||||
UPDATE brand_keywords SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND brand_id = sqlc.arg(brand_id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
|
||||
-- name: SoftDeleteKeywordsByBrand :exec
|
||||
UPDATE brand_keywords SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE brand_id = sqlc.arg(brand_id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
|
||||
-- name: ListQuestions :many
|
||||
SELECT q.id, q.tenant_id, q.brand_id, q.keyword_id, q.current_version_id, q.status, q.created_at,
|
||||
v.question_text, v.version_no
|
||||
FROM brand_questions q
|
||||
LEFT JOIN brand_question_versions v ON v.id = q.current_version_id
|
||||
WHERE q.brand_id = sqlc.arg(brand_id) AND q.tenant_id = sqlc.arg(tenant_id) AND q.deleted_at IS NULL
|
||||
AND (sqlc.narg(keyword_id)::bigint IS NULL OR q.keyword_id = sqlc.narg(keyword_id))
|
||||
ORDER BY q.created_at DESC;
|
||||
|
||||
-- name: CreateQuestion :one
|
||||
INSERT INTO brand_questions (tenant_id, brand_id, keyword_id, status)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.arg(brand_id), sqlc.arg(keyword_id), 'active')
|
||||
RETURNING id;
|
||||
|
||||
-- name: CreateQuestionVersion :one
|
||||
INSERT INTO brand_question_versions (question_id, question_text, question_hash, version_no, is_active)
|
||||
VALUES (sqlc.arg(question_id), sqlc.arg(question_text), sqlc.arg(question_hash), sqlc.arg(version_no), true)
|
||||
RETURNING id;
|
||||
|
||||
-- name: UpdateQuestionCurrentVersion :exec
|
||||
UPDATE brand_questions SET current_version_id = sqlc.arg(version_id), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: DeactivateQuestionVersion :exec
|
||||
UPDATE brand_question_versions SET is_active = false
|
||||
WHERE question_id = sqlc.arg(question_id) AND is_active = true;
|
||||
|
||||
-- name: GetLatestQuestionVersionNo :one
|
||||
SELECT COALESCE(MAX(version_no), 0)::INT AS max_version
|
||||
FROM brand_question_versions
|
||||
WHERE question_id = sqlc.arg(question_id);
|
||||
|
||||
-- name: SoftDeleteQuestion :exec
|
||||
UPDATE brand_questions SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND brand_id = sqlc.arg(brand_id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
|
||||
-- name: SoftDeleteQuestionsByBrand :exec
|
||||
UPDATE brand_questions SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE brand_id = sqlc.arg(brand_id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
|
||||
-- name: ListQuestionVersions :many
|
||||
SELECT v.id, v.question_id, v.question_text, v.question_hash, v.version_no, v.is_active, v.created_at
|
||||
FROM brand_question_versions v
|
||||
JOIN brand_questions q ON q.id = v.question_id
|
||||
WHERE q.brand_id = sqlc.arg(brand_id) AND q.tenant_id = sqlc.arg(tenant_id)
|
||||
ORDER BY v.created_at DESC;
|
||||
|
||||
-- name: ListCompetitors :many
|
||||
SELECT id, tenant_id, brand_id, name, website, description, product_lines_json, created_at, updated_at
|
||||
FROM competitors
|
||||
WHERE brand_id = sqlc.arg(brand_id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: CreateCompetitor :one
|
||||
INSERT INTO competitors (tenant_id, brand_id, name, website, description, product_lines_json)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.arg(brand_id), sqlc.arg(name), sqlc.arg(website), sqlc.arg(description), sqlc.arg(product_lines_json))
|
||||
RETURNING id, created_at;
|
||||
|
||||
-- name: UpdateCompetitor :exec
|
||||
UPDATE competitors SET name = sqlc.arg(name), website = sqlc.arg(website),
|
||||
description = sqlc.arg(description), product_lines_json = sqlc.arg(product_lines_json), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND brand_id = sqlc.arg(brand_id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
|
||||
-- name: SoftDeleteCompetitor :exec
|
||||
UPDATE competitors SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND brand_id = sqlc.arg(brand_id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
|
||||
-- name: SoftDeleteCompetitorsByBrand :exec
|
||||
UPDATE competitors SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE brand_id = sqlc.arg(brand_id) AND tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
@@ -0,0 +1,30 @@
|
||||
-- name: GetCurrentBalance :one
|
||||
SELECT COALESCE(
|
||||
(SELECT balance_after FROM tenant_quota_ledgers
|
||||
WHERE tenant_id = sqlc.arg(tenant_id) AND quota_type = sqlc.arg(quota_type)
|
||||
ORDER BY created_at DESC LIMIT 1), 0
|
||||
)::INT AS balance;
|
||||
|
||||
-- name: InsertQuotaLedger :one
|
||||
INSERT INTO tenant_quota_ledgers (tenant_id, quota_type, delta, balance_after, reason, reference_type, reference_id)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.arg(quota_type), sqlc.arg(delta), sqlc.arg(balance_after),
|
||||
sqlc.arg(reason), sqlc.arg(reference_type), sqlc.arg(reference_id))
|
||||
RETURNING id;
|
||||
|
||||
-- name: CreateQuotaReservation :one
|
||||
INSERT INTO quota_reservations (tenant_id, quota_type, resource_type, resource_id, reserved_amount, status, expire_at)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.arg(quota_type), sqlc.arg(resource_type), sqlc.arg(resource_id),
|
||||
sqlc.arg(reserved_amount), 'pending', sqlc.arg(expire_at))
|
||||
RETURNING id;
|
||||
|
||||
-- name: UpdateQuotaReservationResource :exec
|
||||
UPDATE quota_reservations SET resource_id = sqlc.arg(resource_id), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: ConfirmReservation :exec
|
||||
UPDATE quota_reservations SET status = 'confirmed', consumed_amount = reserved_amount, updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: RefundReservation :exec
|
||||
UPDATE quota_reservations SET status = 'refunded', refunded_amount = reserved_amount, updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
@@ -0,0 +1,22 @@
|
||||
-- name: ListTemplates :many
|
||||
SELECT id, scope, tenant_id, origin_type, template_key, template_name,
|
||||
schema_json, prompt_template, prompt_visibility, card_config_json,
|
||||
status, version_no, created_at, updated_at
|
||||
FROM article_templates
|
||||
WHERE (scope = 'platform' OR tenant_id = sqlc.arg(tenant_id)::bigint)
|
||||
AND status = 'active' AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: GetTemplateByID :one
|
||||
SELECT id, scope, tenant_id, origin_type, template_key, template_name,
|
||||
schema_json, prompt_template, prompt_visibility, protected_prompt_asset_key,
|
||||
card_config_json, status, version_no, created_at, updated_at
|
||||
FROM article_templates
|
||||
WHERE id = sqlc.arg(id)
|
||||
AND (scope = 'platform' OR tenant_id = sqlc.arg(tenant_id)::bigint)
|
||||
AND deleted_at IS NULL;
|
||||
|
||||
-- name: GetTemplateSchema :one
|
||||
SELECT id, template_name, schema_json
|
||||
FROM article_templates
|
||||
WHERE id = sqlc.arg(id) AND deleted_at IS NULL;
|
||||
@@ -0,0 +1,37 @@
|
||||
-- name: CountArticlesByTenant :one
|
||||
SELECT COUNT(*) FROM articles
|
||||
WHERE tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
|
||||
-- name: CountPublishedArticlesByTenant :one
|
||||
SELECT COUNT(*) FROM articles
|
||||
WHERE tenant_id = sqlc.arg(tenant_id) AND publish_status = 'success' AND deleted_at IS NULL;
|
||||
|
||||
-- name: CountBrandsByTenant :one
|
||||
SELECT COUNT(*) FROM brands
|
||||
WHERE tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL;
|
||||
|
||||
-- name: GetRecentArticles :many
|
||||
SELECT a.id, a.generate_status, a.publish_status, a.source_type, a.created_at,
|
||||
av.title, av.word_count, av.source_label,
|
||||
t.template_name
|
||||
FROM articles a
|
||||
LEFT JOIN article_versions av ON av.id = a.current_version_id
|
||||
LEFT JOIN article_templates t ON t.id = a.template_id
|
||||
WHERE a.tenant_id = sqlc.arg(tenant_id) AND a.deleted_at IS NULL
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT 10;
|
||||
|
||||
-- name: GetQuotaSummary :one
|
||||
SELECT COALESCE(
|
||||
(SELECT balance_after FROM tenant_quota_ledgers
|
||||
WHERE tenant_id = sqlc.arg(tenant_id) AND quota_type = 'article_generation'
|
||||
ORDER BY created_at DESC LIMIT 1), 0
|
||||
)::INT AS balance;
|
||||
|
||||
-- name: GetActivePlanForTenant :one
|
||||
SELECT p.plan_code, p.name AS plan_name, p.quota_policy_json,
|
||||
s.start_at, s.end_at
|
||||
FROM tenant_plan_subscriptions s
|
||||
JOIN plans p ON p.id = s.plan_id
|
||||
WHERE s.tenant_id = sqlc.arg(tenant_id) AND s.status = 'active' AND s.deleted_at IS NULL
|
||||
LIMIT 1;
|
||||
@@ -0,0 +1,101 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
|
||||
type QuotaLedgerInput struct {
|
||||
TenantID int64
|
||||
QuotaType string
|
||||
Delta int
|
||||
BalanceAfter int
|
||||
Reason *string
|
||||
ReferenceType *string
|
||||
ReferenceID *int64
|
||||
}
|
||||
|
||||
type QuotaReservationInput struct {
|
||||
TenantID int64
|
||||
QuotaType string
|
||||
ResourceType string
|
||||
ResourceID int64
|
||||
ReservedAmount int
|
||||
ExpireAt time.Time
|
||||
}
|
||||
|
||||
type QuotaRepository interface {
|
||||
GetCurrentBalance(ctx context.Context, tenantID int64, quotaType string) (int, error)
|
||||
InsertQuotaLedger(ctx context.Context, input QuotaLedgerInput) (int64, error)
|
||||
CreateQuotaReservation(ctx context.Context, input QuotaReservationInput) (int64, error)
|
||||
UpdateQuotaReservationResource(ctx context.Context, reservationID, tenantID, resourceID int64) error
|
||||
ConfirmReservation(ctx context.Context, reservationID, tenantID int64) error
|
||||
RefundReservation(ctx context.Context, reservationID, tenantID int64) error
|
||||
}
|
||||
|
||||
type quotaRepository struct {
|
||||
q generated.Querier
|
||||
}
|
||||
|
||||
func NewQuotaRepository(db generated.DBTX) QuotaRepository {
|
||||
return "aRepository{q: newQuerier(db)}
|
||||
}
|
||||
|
||||
func (r *quotaRepository) GetCurrentBalance(ctx context.Context, tenantID int64, quotaType string) (int, error) {
|
||||
balance, err := r.q.GetCurrentBalance(ctx, generated.GetCurrentBalanceParams{
|
||||
TenantID: tenantID,
|
||||
QuotaType: quotaType,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(balance), nil
|
||||
}
|
||||
|
||||
func (r *quotaRepository) InsertQuotaLedger(ctx context.Context, input QuotaLedgerInput) (int64, error) {
|
||||
return r.q.InsertQuotaLedger(ctx, generated.InsertQuotaLedgerParams{
|
||||
TenantID: input.TenantID,
|
||||
QuotaType: input.QuotaType,
|
||||
Delta: int32(input.Delta),
|
||||
BalanceAfter: int32(input.BalanceAfter),
|
||||
Reason: pgText(input.Reason),
|
||||
ReferenceType: pgText(input.ReferenceType),
|
||||
ReferenceID: pgInt8(input.ReferenceID),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *quotaRepository) CreateQuotaReservation(ctx context.Context, input QuotaReservationInput) (int64, error) {
|
||||
expireAt := input.ExpireAt
|
||||
return r.q.CreateQuotaReservation(ctx, generated.CreateQuotaReservationParams{
|
||||
TenantID: input.TenantID,
|
||||
QuotaType: input.QuotaType,
|
||||
ResourceType: input.ResourceType,
|
||||
ResourceID: input.ResourceID,
|
||||
ReservedAmount: int32(input.ReservedAmount),
|
||||
ExpireAt: pgTimestamp(&expireAt),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *quotaRepository) UpdateQuotaReservationResource(ctx context.Context, reservationID, tenantID, resourceID int64) error {
|
||||
return r.q.UpdateQuotaReservationResource(ctx, generated.UpdateQuotaReservationResourceParams{
|
||||
ResourceID: resourceID,
|
||||
ID: reservationID,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *quotaRepository) ConfirmReservation(ctx context.Context, reservationID, tenantID int64) error {
|
||||
return r.q.ConfirmReservation(ctx, generated.ConfirmReservationParams{
|
||||
ID: reservationID,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *quotaRepository) RefundReservation(ctx context.Context, reservationID, tenantID int64) error {
|
||||
return r.q.RefundReservation(ctx, generated.RefundReservationParams{
|
||||
ID: reservationID,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build integration
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Repository integration tests require a running PostgreSQL instance.
|
||||
// Run with: go test -tags=integration ./internal/tenant/repository/...
|
||||
//
|
||||
// Set environment variable:
|
||||
// TEST_DATABASE_URL=postgres://geo:geo_dev@localhost:5432/geo_test?sslmode=disable
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("TEST_DATABASE_URL") == "" {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestTenantIsolation(t *testing.T) {
|
||||
t.Skip("requires running PostgreSQL — run with -tags=integration")
|
||||
}
|
||||
|
||||
func TestSoftDeleteAndReCreate(t *testing.T) {
|
||||
t.Skip("requires running PostgreSQL — run with -tags=integration")
|
||||
}
|
||||
|
||||
func TestQuestionVersioning(t *testing.T) {
|
||||
t.Skip("requires running PostgreSQL — run with -tags=integration")
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
version: "2"
|
||||
sql:
|
||||
- schema: "../../../migrations/"
|
||||
queries: "queries/"
|
||||
engine: "postgresql"
|
||||
gen:
|
||||
go:
|
||||
package: "generated"
|
||||
out: "generated"
|
||||
emit_json_tags: true
|
||||
emit_prepared_queries: false
|
||||
emit_interface: true
|
||||
emit_exact_table_names: false
|
||||
sql_package: "pgx/v5"
|
||||
@@ -0,0 +1,116 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
|
||||
type TemplateRecord struct {
|
||||
ID int64
|
||||
Scope string
|
||||
TenantID *int64
|
||||
OriginType string
|
||||
TemplateKey string
|
||||
TemplateName string
|
||||
SchemaJSON []byte
|
||||
PromptTemplate *string
|
||||
PromptVisibility string
|
||||
ProtectedPromptAssetKey *string
|
||||
CardConfigJSON []byte
|
||||
Status string
|
||||
VersionNo int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type TemplateSchemaRecord struct {
|
||||
ID int64
|
||||
TemplateName string
|
||||
SchemaJSON []byte
|
||||
}
|
||||
|
||||
type TemplateRepository interface {
|
||||
ListTemplates(ctx context.Context, tenantID int64) ([]TemplateRecord, error)
|
||||
GetTemplateByID(ctx context.Context, id, tenantID int64) (*TemplateRecord, error)
|
||||
GetTemplateSchema(ctx context.Context, id int64) (*TemplateSchemaRecord, error)
|
||||
}
|
||||
|
||||
type templateRepository struct {
|
||||
q generated.Querier
|
||||
}
|
||||
|
||||
func NewTemplateRepository(db generated.DBTX) TemplateRepository {
|
||||
return &templateRepository{q: newQuerier(db)}
|
||||
}
|
||||
|
||||
func (r *templateRepository) ListTemplates(ctx context.Context, tenantID int64) ([]TemplateRecord, error) {
|
||||
rows, err := r.q.ListTemplates(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
templates := make([]TemplateRecord, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
templates = append(templates, TemplateRecord{
|
||||
ID: row.ID,
|
||||
Scope: row.Scope,
|
||||
TenantID: nullableInt64(row.TenantID),
|
||||
OriginType: row.OriginType,
|
||||
TemplateKey: row.TemplateKey,
|
||||
TemplateName: row.TemplateName,
|
||||
SchemaJSON: row.SchemaJson,
|
||||
PromptTemplate: nullableText(row.PromptTemplate),
|
||||
PromptVisibility: row.PromptVisibility,
|
||||
CardConfigJSON: row.CardConfigJson,
|
||||
Status: row.Status,
|
||||
VersionNo: int(row.VersionNo),
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
func (r *templateRepository) GetTemplateByID(ctx context.Context, id, tenantID int64) (*TemplateRecord, error) {
|
||||
row, err := r.q.GetTemplateByID(ctx, generated.GetTemplateByIDParams{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TemplateRecord{
|
||||
ID: row.ID,
|
||||
Scope: row.Scope,
|
||||
TenantID: nullableInt64(row.TenantID),
|
||||
OriginType: row.OriginType,
|
||||
TemplateKey: row.TemplateKey,
|
||||
TemplateName: row.TemplateName,
|
||||
SchemaJSON: row.SchemaJson,
|
||||
PromptTemplate: nullableText(row.PromptTemplate),
|
||||
PromptVisibility: row.PromptVisibility,
|
||||
ProtectedPromptAssetKey: nullableText(row.ProtectedPromptAssetKey),
|
||||
CardConfigJSON: row.CardConfigJson,
|
||||
Status: row.Status,
|
||||
VersionNo: int(row.VersionNo),
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *templateRepository) GetTemplateSchema(ctx context.Context, id int64) (*TemplateSchemaRecord, error) {
|
||||
row, err := r.q.GetTemplateSchema(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TemplateSchemaRecord{
|
||||
ID: row.ID,
|
||||
TemplateName: row.TemplateName,
|
||||
SchemaJSON: row.SchemaJson,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
|
||||
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
|
||||
QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
type TxManager struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewTxManager(pool *pgxpool.Pool) *TxManager {
|
||||
return &TxManager{pool: pool}
|
||||
}
|
||||
|
||||
func (m *TxManager) WithTx(ctx context.Context, fn func(tx pgx.Tx) error) error {
|
||||
tx, err := m.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback(ctx)
|
||||
}()
|
||||
|
||||
if err := fn(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (m *TxManager) Pool() *pgxpool.Pool {
|
||||
return m.pool
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
|
||||
type WorkspaceRecentArticle struct {
|
||||
ID int64
|
||||
GenerateStatus string
|
||||
PublishStatus string
|
||||
SourceType string
|
||||
CreatedAt time.Time
|
||||
Title *string
|
||||
WordCount int
|
||||
SourceLabel *string
|
||||
TemplateName *string
|
||||
}
|
||||
|
||||
type WorkspacePlan struct {
|
||||
PlanCode string
|
||||
PlanName string
|
||||
QuotaPolicyJSON []byte
|
||||
StartAt time.Time
|
||||
EndAt time.Time
|
||||
}
|
||||
|
||||
type WorkspaceRepository interface {
|
||||
CountArticlesByTenant(ctx context.Context, tenantID int64) (int64, error)
|
||||
CountPublishedArticlesByTenant(ctx context.Context, tenantID int64) (int64, error)
|
||||
CountBrandsByTenant(ctx context.Context, tenantID int64) (int64, error)
|
||||
GetRecentArticles(ctx context.Context, tenantID int64) ([]WorkspaceRecentArticle, error)
|
||||
GetQuotaSummary(ctx context.Context, tenantID int64) (int, error)
|
||||
GetActivePlanForTenant(ctx context.Context, tenantID int64) (*WorkspacePlan, error)
|
||||
}
|
||||
|
||||
type workspaceRepository struct {
|
||||
q generated.Querier
|
||||
}
|
||||
|
||||
func NewWorkspaceRepository(db generated.DBTX) WorkspaceRepository {
|
||||
return &workspaceRepository{q: newQuerier(db)}
|
||||
}
|
||||
|
||||
func (r *workspaceRepository) CountArticlesByTenant(ctx context.Context, tenantID int64) (int64, error) {
|
||||
return r.q.CountArticlesByTenant(ctx, tenantID)
|
||||
}
|
||||
|
||||
func (r *workspaceRepository) CountPublishedArticlesByTenant(ctx context.Context, tenantID int64) (int64, error) {
|
||||
return r.q.CountPublishedArticlesByTenant(ctx, tenantID)
|
||||
}
|
||||
|
||||
func (r *workspaceRepository) CountBrandsByTenant(ctx context.Context, tenantID int64) (int64, error) {
|
||||
return r.q.CountBrandsByTenant(ctx, tenantID)
|
||||
}
|
||||
|
||||
func (r *workspaceRepository) GetRecentArticles(ctx context.Context, tenantID int64) ([]WorkspaceRecentArticle, error) {
|
||||
rows, err := r.q.GetRecentArticles(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
articles := make([]WorkspaceRecentArticle, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
articles = append(articles, WorkspaceRecentArticle{
|
||||
ID: row.ID,
|
||||
GenerateStatus: row.GenerateStatus,
|
||||
PublishStatus: row.PublishStatus,
|
||||
SourceType: row.SourceType,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
Title: nullableText(row.Title),
|
||||
WordCount: intFromInt4(row.WordCount),
|
||||
SourceLabel: nullableText(row.SourceLabel),
|
||||
TemplateName: nullableText(row.TemplateName),
|
||||
})
|
||||
}
|
||||
|
||||
return articles, nil
|
||||
}
|
||||
|
||||
func (r *workspaceRepository) GetQuotaSummary(ctx context.Context, tenantID int64) (int, error) {
|
||||
balance, err := r.q.GetQuotaSummary(ctx, tenantID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(balance), nil
|
||||
}
|
||||
|
||||
func (r *workspaceRepository) GetActivePlanForTenant(ctx context.Context, tenantID int64) (*WorkspacePlan, error) {
|
||||
row, err := r.q.GetActivePlanForTenant(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &WorkspacePlan{
|
||||
PlanCode: row.PlanCode,
|
||||
PlanName: row.PlanName,
|
||||
QuotaPolicyJSON: row.QuotaPolicyJson,
|
||||
StartAt: timeFromTimestamp(row.StartAt),
|
||||
EndAt: timeFromTimestamp(row.EndAt),
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user