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,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
|
||||
}
|
||||
Reference in New Issue
Block a user