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:
2026-04-01 00:58:42 +08:00
commit de30497f59
210 changed files with 23733 additions and 0 deletions
@@ -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 &quotaRepository{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
}
+43
View File
@@ -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
}