feat: add image management functionality
- Implemented image folder repository with CRUD operations. - Added image reference repository for managing image references to articles. - Created image repository for handling image assets, including listing, inserting, updating, and deleting images. - Introduced image usage repository to track storage usage and quotas for tenants. - Added SQL queries for image assets, folders, references, and usage. - Developed image handler for HTTP endpoints to manage images and folders. - Created database migration scripts for image-related tables and structures.
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: image.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const countImageAssets = `-- name: CountImageAssets :one
|
||||
SELECT COUNT(*)::INT AS total
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
AND (
|
||||
$2::BIGINT IS NULL
|
||||
OR ($2::BIGINT = 0 AND folder_id IS NULL)
|
||||
OR folder_id = $2::BIGINT
|
||||
)
|
||||
AND ($3::TEXT IS NULL OR name ILIKE '%' || $3::TEXT || '%')
|
||||
`
|
||||
|
||||
type CountImageAssetsParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
SearchQuery pgtype.Text `json:"search_query"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountImageAssets(ctx context.Context, arg CountImageAssetsParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, countImageAssets, arg.TenantID, arg.FolderID, arg.SearchQuery)
|
||||
var total int32
|
||||
err := row.Scan(&total)
|
||||
return total, err
|
||||
}
|
||||
|
||||
const countUncategorizedImages = `-- name: CountUncategorizedImages :one
|
||||
SELECT COUNT(*)::INT AS count
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND folder_id IS NULL
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
func (q *Queries) CountUncategorizedImages(ctx context.Context, tenantID int64) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, countUncategorizedImages, tenantID)
|
||||
var count int32
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const getImageAsset = `-- name: GetImageAsset :one
|
||||
SELECT id, tenant_id, folder_id, object_key, name, size_bytes, status, created_by, created_at, updated_at
|
||||
FROM image_assets
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
`
|
||||
|
||||
type GetImageAssetParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
type GetImageAssetRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
Name string `json:"name"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
Status string `json:"status"`
|
||||
CreatedBy int64 `json:"created_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetImageAsset(ctx context.Context, arg GetImageAssetParams) (GetImageAssetRow, error) {
|
||||
row := q.db.QueryRow(ctx, getImageAsset, arg.ID, arg.TenantID)
|
||||
var i GetImageAssetRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.FolderID,
|
||||
&i.ObjectKey,
|
||||
&i.Name,
|
||||
&i.SizeBytes,
|
||||
&i.Status,
|
||||
&i.CreatedBy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertImageAsset = `-- name: InsertImageAsset :one
|
||||
INSERT INTO image_assets (tenant_id, folder_id, object_key, name, size_bytes, status, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type InsertImageAssetParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
Name string `json:"name"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
Status string `json:"status"`
|
||||
CreatedBy int64 `json:"created_by"`
|
||||
}
|
||||
|
||||
func (q *Queries) InsertImageAsset(ctx context.Context, arg InsertImageAssetParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, insertImageAsset,
|
||||
arg.TenantID,
|
||||
arg.FolderID,
|
||||
arg.ObjectKey,
|
||||
arg.Name,
|
||||
arg.SizeBytes,
|
||||
arg.Status,
|
||||
arg.CreatedBy,
|
||||
)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const listActiveImagesByFolder = `-- name: ListActiveImagesByFolder :many
|
||||
SELECT id, object_key, size_bytes
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND folder_id = $2
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type ListActiveImagesByFolderParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
}
|
||||
|
||||
type ListActiveImagesByFolderRow struct {
|
||||
ID int64 `json:"id"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListActiveImagesByFolder(ctx context.Context, arg ListActiveImagesByFolderParams) ([]ListActiveImagesByFolderRow, error) {
|
||||
rows, err := q.db.Query(ctx, listActiveImagesByFolder, arg.TenantID, arg.FolderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListActiveImagesByFolderRow
|
||||
for rows.Next() {
|
||||
var i ListActiveImagesByFolderRow
|
||||
if err := rows.Scan(&i.ID, &i.ObjectKey, &i.SizeBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listImageAssets = `-- name: ListImageAssets :many
|
||||
SELECT id, tenant_id, folder_id, object_key, name, size_bytes, status, created_by, created_at, updated_at
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
AND (
|
||||
$2::BIGINT IS NULL
|
||||
OR ($2::BIGINT = 0 AND folder_id IS NULL)
|
||||
OR folder_id = $2::BIGINT
|
||||
)
|
||||
AND ($3::TEXT IS NULL OR name ILIKE '%' || $3::TEXT || '%')
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $5 OFFSET $4
|
||||
`
|
||||
|
||||
type ListImageAssetsParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
SearchQuery pgtype.Text `json:"search_query"`
|
||||
PageOffset int32 `json:"page_offset"`
|
||||
PageSize int32 `json:"page_size"`
|
||||
}
|
||||
|
||||
type ListImageAssetsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
Name string `json:"name"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
Status string `json:"status"`
|
||||
CreatedBy int64 `json:"created_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListImageAssets(ctx context.Context, arg ListImageAssetsParams) ([]ListImageAssetsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listImageAssets,
|
||||
arg.TenantID,
|
||||
arg.FolderID,
|
||||
arg.SearchQuery,
|
||||
arg.PageOffset,
|
||||
arg.PageSize,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListImageAssetsRow
|
||||
for rows.Next() {
|
||||
var i ListImageAssetsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.FolderID,
|
||||
&i.ObjectKey,
|
||||
&i.Name,
|
||||
&i.SizeBytes,
|
||||
&i.Status,
|
||||
&i.CreatedBy,
|
||||
&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 markImagesInFolderPendingDelete = `-- name: MarkImagesInFolderPendingDelete :exec
|
||||
UPDATE image_assets
|
||||
SET status = 'pending_delete', folder_id = NULL, updated_at = NOW()
|
||||
WHERE tenant_id = $1
|
||||
AND folder_id = $2
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type MarkImagesInFolderPendingDeleteParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) MarkImagesInFolderPendingDelete(ctx context.Context, arg MarkImagesInFolderPendingDeleteParams) error {
|
||||
_, err := q.db.Exec(ctx, markImagesInFolderPendingDelete, arg.TenantID, arg.FolderID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateImageAssetDeleted = `-- name: UpdateImageAssetDeleted :exec
|
||||
UPDATE image_assets
|
||||
SET status = 'deleted', deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
`
|
||||
|
||||
type UpdateImageAssetDeletedParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateImageAssetDeleted(ctx context.Context, arg UpdateImageAssetDeletedParams) error {
|
||||
_, err := q.db.Exec(ctx, updateImageAssetDeleted, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateImageAssetFolder = `-- name: UpdateImageAssetFolder :exec
|
||||
UPDATE image_assets
|
||||
SET folder_id = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
`
|
||||
|
||||
type UpdateImageAssetFolderParams struct {
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateImageAssetFolder(ctx context.Context, arg UpdateImageAssetFolderParams) error {
|
||||
_, err := q.db.Exec(ctx, updateImageAssetFolder, arg.FolderID, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateImageAssetName = `-- name: UpdateImageAssetName :exec
|
||||
UPDATE image_assets
|
||||
SET name = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
`
|
||||
|
||||
type UpdateImageAssetNameParams struct {
|
||||
Name string `json:"name"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateImageAssetName(ctx context.Context, arg UpdateImageAssetNameParams) error {
|
||||
_, err := q.db.Exec(ctx, updateImageAssetName, arg.Name, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateImageAssetStatus = `-- name: UpdateImageAssetStatus :exec
|
||||
UPDATE image_assets
|
||||
SET status = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
`
|
||||
|
||||
type UpdateImageAssetStatusParams struct {
|
||||
Status string `json:"status"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateImageAssetStatus(ctx context.Context, arg UpdateImageAssetStatusParams) error {
|
||||
_, err := q.db.Exec(ctx, updateImageAssetStatus, arg.Status, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: image_folder.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const countActiveImagesInFolder = `-- name: CountActiveImagesInFolder :one
|
||||
SELECT COUNT(*)::INT AS count
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND folder_id = $2
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type CountActiveImagesInFolderParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountActiveImagesInFolder(ctx context.Context, arg CountActiveImagesInFolderParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, countActiveImagesInFolder, arg.TenantID, arg.FolderID)
|
||||
var count int32
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const createImageFolder = `-- name: CreateImageFolder :one
|
||||
INSERT INTO image_folders (tenant_id, name)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type CreateImageFolderParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateImageFolder(ctx context.Context, arg CreateImageFolderParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, createImageFolder, arg.TenantID, arg.Name)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const deleteImageFolder = `-- name: DeleteImageFolder :exec
|
||||
DELETE FROM image_folders
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
`
|
||||
|
||||
type DeleteImageFolderParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteImageFolder(ctx context.Context, arg DeleteImageFolderParams) error {
|
||||
_, err := q.db.Exec(ctx, deleteImageFolder, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getImageFolder = `-- name: GetImageFolder :one
|
||||
SELECT id, tenant_id, name, created_at, updated_at
|
||||
FROM image_folders
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
`
|
||||
|
||||
type GetImageFolderParams struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
type GetImageFolderRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetImageFolder(ctx context.Context, arg GetImageFolderParams) (GetImageFolderRow, error) {
|
||||
row := q.db.QueryRow(ctx, getImageFolder, arg.ID, arg.TenantID)
|
||||
var i GetImageFolderRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.Name,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listImageFolders = `-- name: ListImageFolders :many
|
||||
SELECT f.id, f.tenant_id, f.name, f.created_at, f.updated_at,
|
||||
COUNT(ia.id) FILTER (WHERE ia.status = 'active' AND ia.deleted_at IS NULL)::INT AS image_count
|
||||
FROM image_folders f
|
||||
LEFT JOIN image_assets ia ON ia.tenant_id = f.tenant_id AND ia.folder_id = f.id
|
||||
WHERE f.tenant_id = $1
|
||||
GROUP BY f.id
|
||||
ORDER BY f.name
|
||||
`
|
||||
|
||||
type ListImageFoldersRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
ImageCount int32 `json:"image_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListImageFolders(ctx context.Context, tenantID int64) ([]ListImageFoldersRow, error) {
|
||||
rows, err := q.db.Query(ctx, listImageFolders, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListImageFoldersRow
|
||||
for rows.Next() {
|
||||
var i ListImageFoldersRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.Name,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ImageCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateImageFolder = `-- name: UpdateImageFolder :exec
|
||||
UPDATE image_folders
|
||||
SET name = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
`
|
||||
|
||||
type UpdateImageFolderParams struct {
|
||||
Name string `json:"name"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateImageFolder(ctx context.Context, arg UpdateImageFolderParams) error {
|
||||
_, err := q.db.Exec(ctx, updateImageFolder, arg.Name, arg.ID, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: image_reference.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const countImageReferences = `-- name: CountImageReferences :one
|
||||
SELECT COUNT(*)::INT AS total
|
||||
FROM image_asset_references
|
||||
WHERE tenant_id = $1 AND image_asset_id = $2
|
||||
`
|
||||
|
||||
type CountImageReferencesParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
ImageAssetID int64 `json:"image_asset_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountImageReferences(ctx context.Context, arg CountImageReferencesParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, countImageReferences, arg.TenantID, arg.ImageAssetID)
|
||||
var total int32
|
||||
err := row.Scan(&total)
|
||||
return total, err
|
||||
}
|
||||
|
||||
const deleteImageReferencesByArticle = `-- name: DeleteImageReferencesByArticle :exec
|
||||
DELETE FROM image_asset_references
|
||||
WHERE tenant_id = $1 AND article_id = $2
|
||||
`
|
||||
|
||||
type DeleteImageReferencesByArticleParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
ArticleID int64 `json:"article_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteImageReferencesByArticle(ctx context.Context, arg DeleteImageReferencesByArticleParams) error {
|
||||
_, err := q.db.Exec(ctx, deleteImageReferencesByArticle, arg.TenantID, arg.ArticleID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteImageReferencesByArticleScope = `-- name: DeleteImageReferencesByArticleScope :exec
|
||||
DELETE FROM image_asset_references
|
||||
WHERE tenant_id = $1 AND article_id = $2 AND ref_scope = $3
|
||||
`
|
||||
|
||||
type DeleteImageReferencesByArticleScopeParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
ArticleID int64 `json:"article_id"`
|
||||
RefScope string `json:"ref_scope"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteImageReferencesByArticleScope(ctx context.Context, arg DeleteImageReferencesByArticleScopeParams) error {
|
||||
_, err := q.db.Exec(ctx, deleteImageReferencesByArticleScope, arg.TenantID, arg.ArticleID, arg.RefScope)
|
||||
return err
|
||||
}
|
||||
|
||||
const listImageReferenceArticles = `-- name: ListImageReferenceArticles :many
|
||||
SELECT DISTINCT r.article_id, r.ref_scope
|
||||
FROM image_asset_references r
|
||||
WHERE r.tenant_id = $1 AND r.image_asset_id = $2
|
||||
`
|
||||
|
||||
type ListImageReferenceArticlesParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
ImageAssetID int64 `json:"image_asset_id"`
|
||||
}
|
||||
|
||||
type ListImageReferenceArticlesRow struct {
|
||||
ArticleID int64 `json:"article_id"`
|
||||
RefScope string `json:"ref_scope"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListImageReferenceArticles(ctx context.Context, arg ListImageReferenceArticlesParams) ([]ListImageReferenceArticlesRow, error) {
|
||||
rows, err := q.db.Query(ctx, listImageReferenceArticles, arg.TenantID, arg.ImageAssetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListImageReferenceArticlesRow
|
||||
for rows.Next() {
|
||||
var i ListImageReferenceArticlesRow
|
||||
if err := rows.Scan(&i.ArticleID, &i.RefScope); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listImageReferences = `-- name: ListImageReferences :many
|
||||
SELECT r.id, r.tenant_id, r.image_asset_id, r.article_id, r.ref_scope, r.created_at
|
||||
FROM image_asset_references r
|
||||
WHERE r.tenant_id = $1 AND r.image_asset_id = $2
|
||||
`
|
||||
|
||||
type ListImageReferencesParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
ImageAssetID int64 `json:"image_asset_id"`
|
||||
}
|
||||
|
||||
type ListImageReferencesRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
ImageAssetID int64 `json:"image_asset_id"`
|
||||
ArticleID int64 `json:"article_id"`
|
||||
RefScope string `json:"ref_scope"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListImageReferences(ctx context.Context, arg ListImageReferencesParams) ([]ListImageReferencesRow, error) {
|
||||
rows, err := q.db.Query(ctx, listImageReferences, arg.TenantID, arg.ImageAssetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListImageReferencesRow
|
||||
for rows.Next() {
|
||||
var i ListImageReferencesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.ImageAssetID,
|
||||
&i.ArticleID,
|
||||
&i.RefScope,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const upsertImageReference = `-- name: UpsertImageReference :exec
|
||||
INSERT INTO image_asset_references (tenant_id, image_asset_id, article_id, ref_scope)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (image_asset_id, article_id, ref_scope) DO UPDATE SET updated_at = NOW()
|
||||
`
|
||||
|
||||
type UpsertImageReferenceParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
ImageAssetID int64 `json:"image_asset_id"`
|
||||
ArticleID int64 `json:"article_id"`
|
||||
RefScope string `json:"ref_scope"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertImageReference(ctx context.Context, arg UpsertImageReferenceParams) error {
|
||||
_, err := q.db.Exec(ctx, upsertImageReference,
|
||||
arg.TenantID,
|
||||
arg.ImageAssetID,
|
||||
arg.ArticleID,
|
||||
arg.RefScope,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: image_usage.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const ensureImageStorageUsageRow = `-- name: EnsureImageStorageUsageRow :exec
|
||||
INSERT INTO tenant_image_storage_usage (tenant_id, used_bytes)
|
||||
VALUES ($1, 0)
|
||||
ON CONFLICT (tenant_id) DO NOTHING
|
||||
`
|
||||
|
||||
func (q *Queries) EnsureImageStorageUsageRow(ctx context.Context, tenantID int64) error {
|
||||
_, err := q.db.Exec(ctx, ensureImageStorageUsageRow, tenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getImageStorageUsage = `-- name: GetImageStorageUsage :one
|
||||
SELECT used_bytes, updated_at
|
||||
FROM tenant_image_storage_usage
|
||||
WHERE tenant_id = $1
|
||||
`
|
||||
|
||||
type GetImageStorageUsageRow struct {
|
||||
UsedBytes int64 `json:"used_bytes"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetImageStorageUsage(ctx context.Context, tenantID int64) (GetImageStorageUsageRow, error) {
|
||||
row := q.db.QueryRow(ctx, getImageStorageUsage, tenantID)
|
||||
var i GetImageStorageUsageRow
|
||||
err := row.Scan(&i.UsedBytes, &i.UpdatedAt)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const releaseImageStorageQuota = `-- name: ReleaseImageStorageQuota :exec
|
||||
UPDATE tenant_image_storage_usage
|
||||
SET used_bytes = GREATEST(0, used_bytes - $1),
|
||||
updated_at = NOW()
|
||||
WHERE tenant_id = $2
|
||||
`
|
||||
|
||||
type ReleaseImageStorageQuotaParams struct {
|
||||
ReleaseBytes int64 `json:"release_bytes"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) ReleaseImageStorageQuota(ctx context.Context, arg ReleaseImageStorageQuotaParams) error {
|
||||
_, err := q.db.Exec(ctx, releaseImageStorageQuota, arg.ReleaseBytes, arg.TenantID)
|
||||
return err
|
||||
}
|
||||
@@ -136,6 +136,38 @@ type GenerationTask struct {
|
||||
OperatorID pgtype.Int8 `json:"operator_id"`
|
||||
}
|
||||
|
||||
type ImageAsset struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
FolderID pgtype.Int8 `json:"folder_id"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
Name string `json:"name"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
Status string `json:"status"`
|
||||
CreatedBy int64 `json:"created_by"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type ImageAssetReference struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
ImageAssetID int64 `json:"image_asset_id"`
|
||||
ArticleID int64 `json:"article_id"`
|
||||
RefScope string `json:"ref_scope"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type ImageFolder struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type KnowledgeChunksMetum struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
@@ -357,20 +389,23 @@ type QuotaReservation struct {
|
||||
}
|
||||
|
||||
type ScheduleTask struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
PromptRuleID int64 `json:"prompt_rule_id"`
|
||||
BrandID pgtype.Int8 `json:"brand_id"`
|
||||
Name string `json:"name"`
|
||||
CronExpr string `json:"cron_expr"`
|
||||
TargetPlatform pgtype.Text `json:"target_platform"`
|
||||
StartAt pgtype.Timestamptz `json:"start_at"`
|
||||
EndAt pgtype.Timestamptz `json:"end_at"`
|
||||
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
PromptRuleID int64 `json:"prompt_rule_id"`
|
||||
BrandID pgtype.Int8 `json:"brand_id"`
|
||||
Name string `json:"name"`
|
||||
CronExpr string `json:"cron_expr"`
|
||||
TargetPlatform pgtype.Text `json:"target_platform"`
|
||||
StartAt pgtype.Timestamptz `json:"start_at"`
|
||||
EndAt pgtype.Timestamptz `json:"end_at"`
|
||||
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
OperatorID pgtype.Int8 `json:"operator_id"`
|
||||
EnableWebSearch bool `json:"enable_web_search"`
|
||||
GenerateCount int32 `json:"generate_count"`
|
||||
}
|
||||
|
||||
type TaskRecord struct {
|
||||
@@ -414,6 +449,12 @@ type Tenant struct {
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type TenantImageStorageUsage struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
UsedBytes int64 `json:"used_bytes"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type TenantMembership struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
@@ -424,6 +465,20 @@ type TenantMembership struct {
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type TenantMonitoringQuota struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
MaxBrands int32 `json:"max_brands"`
|
||||
CollectionMode string `json:"collection_mode"`
|
||||
QuestionSelectionMode string `json:"question_selection_mode"`
|
||||
CollectFrequency string `json:"collect_frequency"`
|
||||
EnabledPlatforms []byte `json:"enabled_platforms"`
|
||||
PrimaryInstallationID pgtype.Int8 `json:"primary_installation_id"`
|
||||
TaskDailyHardCap int32 `json:"task_daily_hard_cap"`
|
||||
PlanTier string `json:"plan_tier"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type TenantPlanSubscription struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
|
||||
@@ -10,30 +10,42 @@ import (
|
||||
|
||||
type Querier interface {
|
||||
ConfirmReservation(ctx context.Context, arg ConfirmReservationParams) error
|
||||
CountActiveImagesInFolder(ctx context.Context, arg CountActiveImagesInFolderParams) (int32, 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)
|
||||
CountImageAssets(ctx context.Context, arg CountImageAssetsParams) (int32, error)
|
||||
CountImageReferences(ctx context.Context, arg CountImageReferencesParams) (int32, error)
|
||||
CountPromptRules(ctx context.Context, arg CountPromptRulesParams) (int64, error)
|
||||
CountPromptRulesUngrouped(ctx context.Context, tenantID int64) (int64, error)
|
||||
CountPublishedArticlesByTenant(ctx context.Context, tenantID int64) (int64, error)
|
||||
CountScheduleTasks(ctx context.Context, arg CountScheduleTasksParams) (int64, error)
|
||||
CountUncategorizedImages(ctx context.Context, tenantID int64) (int32, 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)
|
||||
CreateImageFolder(ctx context.Context, arg CreateImageFolderParams) (int64, error)
|
||||
CreateKeyword(ctx context.Context, arg CreateKeywordParams) (CreateKeywordRow, error)
|
||||
CreatePromptRule(ctx context.Context, arg CreatePromptRuleParams) (CreatePromptRuleRow, error)
|
||||
CreatePromptRuleGroup(ctx context.Context, arg CreatePromptRuleGroupParams) (CreatePromptRuleGroupRow, error)
|
||||
CreateQuestion(ctx context.Context, arg CreateQuestionParams) (int64, error)
|
||||
CreateQuotaReservation(ctx context.Context, arg CreateQuotaReservationParams) (int64, error)
|
||||
CreateScheduleTask(ctx context.Context, arg CreateScheduleTaskParams) (CreateScheduleTaskRow, error)
|
||||
DeleteImageFolder(ctx context.Context, arg DeleteImageFolderParams) error
|
||||
DeleteImageReferencesByArticle(ctx context.Context, arg DeleteImageReferencesByArticleParams) error
|
||||
DeleteImageReferencesByArticleScope(ctx context.Context, arg DeleteImageReferencesByArticleScopeParams) error
|
||||
EnsureImageStorageUsageRow(ctx context.Context, tenantID 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)
|
||||
GetImageAsset(ctx context.Context, arg GetImageAssetParams) (GetImageAssetRow, error)
|
||||
GetImageFolder(ctx context.Context, arg GetImageFolderParams) (GetImageFolderRow, error)
|
||||
GetImageStorageUsage(ctx context.Context, tenantID int64) (GetImageStorageUsageRow, error)
|
||||
GetPromptRuleByID(ctx context.Context, arg GetPromptRuleByIDParams) (GetPromptRuleByIDRow, error)
|
||||
GetQuotaSummary(ctx context.Context, tenantID int64) (int32, error)
|
||||
GetRecentArticles(ctx context.Context, tenantID int64) ([]GetRecentArticlesRow, error)
|
||||
@@ -43,11 +55,17 @@ type Querier interface {
|
||||
GetTenantMembershipByTenantAndUser(ctx context.Context, arg GetTenantMembershipByTenantAndUserParams) (GetTenantMembershipByTenantAndUserRow, error)
|
||||
GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error)
|
||||
GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, error)
|
||||
InsertImageAsset(ctx context.Context, arg InsertImageAssetParams) (int64, error)
|
||||
InsertQuotaLedger(ctx context.Context, arg InsertQuotaLedgerParams) (int64, error)
|
||||
ListActiveImagesByFolder(ctx context.Context, arg ListActiveImagesByFolderParams) ([]ListActiveImagesByFolderRow, error)
|
||||
ListAllPromptRulesSimple(ctx context.Context, tenantID int64) ([]ListAllPromptRulesSimpleRow, 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)
|
||||
ListImageAssets(ctx context.Context, arg ListImageAssetsParams) ([]ListImageAssetsRow, error)
|
||||
ListImageFolders(ctx context.Context, tenantID int64) ([]ListImageFoldersRow, error)
|
||||
ListImageReferenceArticles(ctx context.Context, arg ListImageReferenceArticlesParams) ([]ListImageReferenceArticlesRow, error)
|
||||
ListImageReferences(ctx context.Context, arg ListImageReferencesParams) ([]ListImageReferencesRow, error)
|
||||
ListKeywords(ctx context.Context, arg ListKeywordsParams) ([]ListKeywordsRow, error)
|
||||
// ============================================================
|
||||
// Prompt Rule Groups
|
||||
@@ -61,7 +79,9 @@ type Querier interface {
|
||||
ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]ListQuestionsRow, error)
|
||||
ListScheduleTasks(ctx context.Context, arg ListScheduleTasksParams) ([]ListScheduleTasksRow, error)
|
||||
ListTemplates(ctx context.Context, tenantID int64) ([]ListTemplatesRow, error)
|
||||
MarkImagesInFolderPendingDelete(ctx context.Context, arg MarkImagesInFolderPendingDeleteParams) error
|
||||
RefundReservation(ctx context.Context, arg RefundReservationParams) error
|
||||
ReleaseImageStorageQuota(ctx context.Context, arg ReleaseImageStorageQuotaParams) error
|
||||
SoftDeleteArticle(ctx context.Context, arg SoftDeleteArticleParams) error
|
||||
SoftDeleteBrand(ctx context.Context, arg SoftDeleteBrandParams) error
|
||||
SoftDeleteCompetitor(ctx context.Context, arg SoftDeleteCompetitorParams) error
|
||||
@@ -78,6 +98,11 @@ type Querier interface {
|
||||
UpdateBrand(ctx context.Context, arg UpdateBrandParams) error
|
||||
UpdateCompetitor(ctx context.Context, arg UpdateCompetitorParams) error
|
||||
UpdateGenerationTaskStatus(ctx context.Context, arg UpdateGenerationTaskStatusParams) error
|
||||
UpdateImageAssetDeleted(ctx context.Context, arg UpdateImageAssetDeletedParams) error
|
||||
UpdateImageAssetFolder(ctx context.Context, arg UpdateImageAssetFolderParams) error
|
||||
UpdateImageAssetName(ctx context.Context, arg UpdateImageAssetNameParams) error
|
||||
UpdateImageAssetStatus(ctx context.Context, arg UpdateImageAssetStatusParams) error
|
||||
UpdateImageFolder(ctx context.Context, arg UpdateImageFolderParams) error
|
||||
UpdateKeyword(ctx context.Context, arg UpdateKeywordParams) error
|
||||
UpdatePromptRule(ctx context.Context, arg UpdatePromptRuleParams) error
|
||||
UpdatePromptRuleGroup(ctx context.Context, arg UpdatePromptRuleGroupParams) error
|
||||
@@ -86,6 +111,7 @@ type Querier interface {
|
||||
UpdateQuotaReservationResource(ctx context.Context, arg UpdateQuotaReservationResourceParams) error
|
||||
UpdateScheduleTask(ctx context.Context, arg UpdateScheduleTaskParams) error
|
||||
UpdateScheduleTaskStatus(ctx context.Context, arg UpdateScheduleTaskStatusParams) error
|
||||
UpsertImageReference(ctx context.Context, arg UpsertImageReferenceParams) error
|
||||
}
|
||||
|
||||
var _ Querier = (*Queries)(nil)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
|
||||
type ImageFolder struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
Name string
|
||||
ImageCount int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type ImageFolderRepository interface {
|
||||
List(ctx context.Context, tenantID int64) ([]ImageFolder, error)
|
||||
Get(ctx context.Context, tenantID, id int64) (*ImageFolder, error)
|
||||
Create(ctx context.Context, tenantID int64, name string) (int64, error)
|
||||
Update(ctx context.Context, tenantID, id int64, name string) error
|
||||
Delete(ctx context.Context, tenantID, id int64) error
|
||||
CountActiveImages(ctx context.Context, tenantID, folderID int64) (int, error)
|
||||
}
|
||||
|
||||
type imageFolderRepository struct {
|
||||
q generated.Querier
|
||||
}
|
||||
|
||||
func NewImageFolderRepository(db generated.DBTX) ImageFolderRepository {
|
||||
return &imageFolderRepository{q: newQuerier(db)}
|
||||
}
|
||||
|
||||
func (r *imageFolderRepository) List(ctx context.Context, tenantID int64) ([]ImageFolder, error) {
|
||||
rows, err := r.q.ListImageFolders(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folders := make([]ImageFolder, len(rows))
|
||||
for i, row := range rows {
|
||||
folders[i] = ImageFolder{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
Name: row.Name,
|
||||
ImageCount: int(row.ImageCount),
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
}
|
||||
}
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
func (r *imageFolderRepository) Get(ctx context.Context, tenantID, id int64) (*ImageFolder, error) {
|
||||
row, err := r.q.GetImageFolder(ctx, generated.GetImageFolderParams{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ImageFolder{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
Name: row.Name,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *imageFolderRepository) Create(ctx context.Context, tenantID int64, name string) (int64, error) {
|
||||
return r.q.CreateImageFolder(ctx, generated.CreateImageFolderParams{
|
||||
TenantID: tenantID,
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageFolderRepository) Update(ctx context.Context, tenantID, id int64, name string) error {
|
||||
return r.q.UpdateImageFolder(ctx, generated.UpdateImageFolderParams{
|
||||
Name: name,
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageFolderRepository) Delete(ctx context.Context, tenantID, id int64) error {
|
||||
return r.q.DeleteImageFolder(ctx, generated.DeleteImageFolderParams{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageFolderRepository) CountActiveImages(ctx context.Context, tenantID, folderID int64) (int, error) {
|
||||
count, err := r.q.CountActiveImagesInFolder(ctx, generated.CountActiveImagesInFolderParams{
|
||||
TenantID: tenantID,
|
||||
FolderID: pgInt8(&folderID),
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(count), nil
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
|
||||
type ImageReference struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
ImageAssetID int64
|
||||
ArticleID int64
|
||||
RefScope string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type ImageReferenceArticle struct {
|
||||
ArticleID int64
|
||||
RefScope string
|
||||
}
|
||||
|
||||
type ImageReferenceRepository interface {
|
||||
List(ctx context.Context, tenantID, imageAssetID int64) ([]ImageReference, error)
|
||||
Count(ctx context.Context, tenantID, imageAssetID int64) (int, error)
|
||||
Upsert(ctx context.Context, tenantID, imageAssetID, articleID int64, refScope string) error
|
||||
DeleteByArticle(ctx context.Context, tenantID, articleID int64) error
|
||||
DeleteByArticleScope(ctx context.Context, tenantID, articleID int64, refScope string) error
|
||||
ListArticles(ctx context.Context, tenantID, imageAssetID int64) ([]ImageReferenceArticle, error)
|
||||
}
|
||||
|
||||
type imageReferenceRepository struct {
|
||||
q generated.Querier
|
||||
}
|
||||
|
||||
func NewImageReferenceRepository(db generated.DBTX) ImageReferenceRepository {
|
||||
return &imageReferenceRepository{q: newQuerier(db)}
|
||||
}
|
||||
|
||||
func (r *imageReferenceRepository) List(ctx context.Context, tenantID, imageAssetID int64) ([]ImageReference, error) {
|
||||
rows, err := r.q.ListImageReferences(ctx, generated.ListImageReferencesParams{
|
||||
TenantID: tenantID,
|
||||
ImageAssetID: imageAssetID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refs := make([]ImageReference, len(rows))
|
||||
for i, row := range rows {
|
||||
refs[i] = ImageReference{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
ImageAssetID: row.ImageAssetID,
|
||||
ArticleID: row.ArticleID,
|
||||
RefScope: row.RefScope,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
}
|
||||
}
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func (r *imageReferenceRepository) Count(ctx context.Context, tenantID, imageAssetID int64) (int, error) {
|
||||
total, err := r.q.CountImageReferences(ctx, generated.CountImageReferencesParams{
|
||||
TenantID: tenantID,
|
||||
ImageAssetID: imageAssetID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(total), nil
|
||||
}
|
||||
|
||||
func (r *imageReferenceRepository) Upsert(ctx context.Context, tenantID, imageAssetID, articleID int64, refScope string) error {
|
||||
return r.q.UpsertImageReference(ctx, generated.UpsertImageReferenceParams{
|
||||
TenantID: tenantID,
|
||||
ImageAssetID: imageAssetID,
|
||||
ArticleID: articleID,
|
||||
RefScope: refScope,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageReferenceRepository) DeleteByArticle(ctx context.Context, tenantID, articleID int64) error {
|
||||
return r.q.DeleteImageReferencesByArticle(ctx, generated.DeleteImageReferencesByArticleParams{
|
||||
TenantID: tenantID,
|
||||
ArticleID: articleID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageReferenceRepository) DeleteByArticleScope(ctx context.Context, tenantID, articleID int64, refScope string) error {
|
||||
return r.q.DeleteImageReferencesByArticleScope(ctx, generated.DeleteImageReferencesByArticleScopeParams{
|
||||
TenantID: tenantID,
|
||||
ArticleID: articleID,
|
||||
RefScope: refScope,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageReferenceRepository) ListArticles(ctx context.Context, tenantID, imageAssetID int64) ([]ImageReferenceArticle, error) {
|
||||
rows, err := r.q.ListImageReferenceArticles(ctx, generated.ListImageReferenceArticlesParams{
|
||||
TenantID: tenantID,
|
||||
ImageAssetID: imageAssetID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
articles := make([]ImageReferenceArticle, len(rows))
|
||||
for i, row := range rows {
|
||||
articles[i] = ImageReferenceArticle{
|
||||
ArticleID: row.ArticleID,
|
||||
RefScope: row.RefScope,
|
||||
}
|
||||
}
|
||||
return articles, nil
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
|
||||
type ImageAsset struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
FolderID *int64
|
||||
ObjectKey string
|
||||
Name string
|
||||
SizeBytes int64
|
||||
Status string
|
||||
CreatedBy int64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type ImageAssetListParams struct {
|
||||
TenantID int64
|
||||
FolderID *int64
|
||||
SearchQuery *string
|
||||
PageSize int
|
||||
PageOffset int
|
||||
}
|
||||
|
||||
type InsertImageAssetInput struct {
|
||||
TenantID int64
|
||||
FolderID *int64
|
||||
ObjectKey string
|
||||
Name string
|
||||
SizeBytes int64
|
||||
Status string
|
||||
CreatedBy int64
|
||||
}
|
||||
|
||||
type FolderImageSummary struct {
|
||||
ID int64
|
||||
ObjectKey string
|
||||
SizeBytes int64
|
||||
}
|
||||
|
||||
type ImageRepository interface {
|
||||
List(ctx context.Context, params ImageAssetListParams) ([]ImageAsset, int, error)
|
||||
Get(ctx context.Context, tenantID, id int64) (*ImageAsset, error)
|
||||
Insert(ctx context.Context, input InsertImageAssetInput) (int64, error)
|
||||
UpdateStatus(ctx context.Context, tenantID, id int64, status string) error
|
||||
MarkDeleted(ctx context.Context, tenantID, id int64) error
|
||||
UpdateName(ctx context.Context, tenantID, id int64, name string) error
|
||||
UpdateFolder(ctx context.Context, tenantID, id int64, folderID *int64) error
|
||||
ListActiveByFolder(ctx context.Context, tenantID, folderID int64) ([]FolderImageSummary, error)
|
||||
MarkImagesInFolderPendingDelete(ctx context.Context, tenantID, folderID int64) error
|
||||
CountUncategorized(ctx context.Context, tenantID int64) (int, error)
|
||||
}
|
||||
|
||||
type imageRepository struct {
|
||||
q generated.Querier
|
||||
}
|
||||
|
||||
func NewImageRepository(db generated.DBTX) ImageRepository {
|
||||
return &imageRepository{q: newQuerier(db)}
|
||||
}
|
||||
|
||||
func (r *imageRepository) List(ctx context.Context, params ImageAssetListParams) ([]ImageAsset, int, error) {
|
||||
listParams := generated.ListImageAssetsParams{
|
||||
TenantID: params.TenantID,
|
||||
FolderID: pgInt8(params.FolderID),
|
||||
PageOffset: int32(params.PageOffset),
|
||||
PageSize: int32(params.PageSize),
|
||||
SearchQuery: pgText(params.SearchQuery),
|
||||
}
|
||||
|
||||
rows, err := r.q.ListImageAssets(ctx, listParams)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
total, err := r.q.CountImageAssets(ctx, generated.CountImageAssetsParams{
|
||||
TenantID: params.TenantID,
|
||||
FolderID: listParams.FolderID,
|
||||
SearchQuery: listParams.SearchQuery,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
assets := make([]ImageAsset, len(rows))
|
||||
for i, row := range rows {
|
||||
assets[i] = ImageAsset{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
FolderID: nullableInt64(row.FolderID),
|
||||
ObjectKey: row.ObjectKey,
|
||||
Name: row.Name,
|
||||
SizeBytes: row.SizeBytes,
|
||||
Status: row.Status,
|
||||
CreatedBy: row.CreatedBy,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
return assets, int(total), nil
|
||||
}
|
||||
|
||||
func (r *imageRepository) Get(ctx context.Context, tenantID, id int64) (*ImageAsset, error) {
|
||||
row, err := r.q.GetImageAsset(ctx, generated.GetImageAssetParams{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ImageAsset{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
FolderID: nullableInt64(row.FolderID),
|
||||
ObjectKey: row.ObjectKey,
|
||||
Name: row.Name,
|
||||
SizeBytes: row.SizeBytes,
|
||||
Status: row.Status,
|
||||
CreatedBy: row.CreatedBy,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *imageRepository) Insert(ctx context.Context, input InsertImageAssetInput) (int64, error) {
|
||||
return r.q.InsertImageAsset(ctx, generated.InsertImageAssetParams{
|
||||
TenantID: input.TenantID,
|
||||
FolderID: pgInt8(input.FolderID),
|
||||
ObjectKey: input.ObjectKey,
|
||||
Name: input.Name,
|
||||
SizeBytes: input.SizeBytes,
|
||||
Status: input.Status,
|
||||
CreatedBy: input.CreatedBy,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageRepository) UpdateStatus(ctx context.Context, tenantID, id int64, status string) error {
|
||||
return r.q.UpdateImageAssetStatus(ctx, generated.UpdateImageAssetStatusParams{
|
||||
Status: status,
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageRepository) MarkDeleted(ctx context.Context, tenantID, id int64) error {
|
||||
return r.q.UpdateImageAssetDeleted(ctx, generated.UpdateImageAssetDeletedParams{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageRepository) UpdateName(ctx context.Context, tenantID, id int64, name string) error {
|
||||
return r.q.UpdateImageAssetName(ctx, generated.UpdateImageAssetNameParams{
|
||||
Name: name,
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageRepository) UpdateFolder(ctx context.Context, tenantID, id int64, folderID *int64) error {
|
||||
return r.q.UpdateImageAssetFolder(ctx, generated.UpdateImageAssetFolderParams{
|
||||
FolderID: pgInt8(folderID),
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageRepository) ListActiveByFolder(ctx context.Context, tenantID, folderID int64) ([]FolderImageSummary, error) {
|
||||
rows, err := r.q.ListActiveImagesByFolder(ctx, generated.ListActiveImagesByFolderParams{
|
||||
TenantID: tenantID,
|
||||
FolderID: pgInt8(&folderID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]FolderImageSummary, len(rows))
|
||||
for i, row := range rows {
|
||||
result[i] = FolderImageSummary{
|
||||
ID: row.ID,
|
||||
ObjectKey: row.ObjectKey,
|
||||
SizeBytes: row.SizeBytes,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *imageRepository) MarkImagesInFolderPendingDelete(ctx context.Context, tenantID, folderID int64) error {
|
||||
return r.q.MarkImagesInFolderPendingDelete(ctx, generated.MarkImagesInFolderPendingDeleteParams{
|
||||
TenantID: tenantID,
|
||||
FolderID: pgInt8(&folderID),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageRepository) CountUncategorized(ctx context.Context, tenantID int64) (int, error) {
|
||||
count, err := r.q.CountUncategorizedImages(ctx, tenantID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(count), nil
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
|
||||
type ImageStorageUsage struct {
|
||||
UsedBytes int64
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type QuotaReserveResult struct {
|
||||
UsedBytes int64
|
||||
QuotaBytes int64
|
||||
}
|
||||
|
||||
type ImageUsageRepository interface {
|
||||
EnsureRow(ctx context.Context, tenantID int64) error
|
||||
Get(ctx context.Context, tenantID int64) (*ImageStorageUsage, error)
|
||||
Release(ctx context.Context, tenantID, releaseBytes int64) error
|
||||
AtomicReserve(ctx context.Context, tenantID, sizeBytes int64) (*QuotaReserveResult, error)
|
||||
}
|
||||
|
||||
type imageUsageRepository struct {
|
||||
q generated.Querier
|
||||
db generated.DBTX
|
||||
}
|
||||
|
||||
func NewImageUsageRepository(db generated.DBTX) ImageUsageRepository {
|
||||
return &imageUsageRepository{q: newQuerier(db), db: db}
|
||||
}
|
||||
|
||||
func (r *imageUsageRepository) EnsureRow(ctx context.Context, tenantID int64) error {
|
||||
return r.q.EnsureImageStorageUsageRow(ctx, tenantID)
|
||||
}
|
||||
|
||||
func (r *imageUsageRepository) Get(ctx context.Context, tenantID int64) (*ImageStorageUsage, error) {
|
||||
row, err := r.q.GetImageStorageUsage(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ImageStorageUsage{
|
||||
UsedBytes: row.UsedBytes,
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *imageUsageRepository) Release(ctx context.Context, tenantID, releaseBytes int64) error {
|
||||
return r.q.ReleaseImageStorageQuota(ctx, generated.ReleaseImageStorageQuotaParams{
|
||||
ReleaseBytes: releaseBytes,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *imageUsageRepository) AtomicReserve(ctx context.Context, tenantID, sizeBytes int64) (*QuotaReserveResult, error) {
|
||||
row := r.db.QueryRow(ctx, `
|
||||
WITH plan AS (
|
||||
SELECT COALESCE((p.quota_policy_json ->> 'image_storage_bytes')::BIGINT, 104857600) AS quota_bytes
|
||||
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
|
||||
)
|
||||
UPDATE tenant_image_storage_usage u
|
||||
SET used_bytes = u.used_bytes + $2,
|
||||
updated_at = NOW()
|
||||
FROM plan
|
||||
WHERE u.tenant_id = $1
|
||||
AND u.used_bytes + $2 <= plan.quota_bytes
|
||||
RETURNING u.used_bytes, plan.quota_bytes
|
||||
`, tenantID, sizeBytes)
|
||||
|
||||
var result QuotaReserveResult
|
||||
if err := row.Scan(&result.UsedBytes, &result.QuotaBytes); err != nil {
|
||||
return nil, fmt.Errorf("reserve image storage quota: %w", err)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
-- name: ListImageAssets :many
|
||||
SELECT id, tenant_id, folder_id, object_key, name, size_bytes, status, created_by, created_at, updated_at
|
||||
FROM image_assets
|
||||
WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
AND (
|
||||
sqlc.narg(folder_id)::BIGINT IS NULL
|
||||
OR (sqlc.narg(folder_id)::BIGINT = 0 AND folder_id IS NULL)
|
||||
OR folder_id = sqlc.narg(folder_id)::BIGINT
|
||||
)
|
||||
AND (sqlc.narg(search_query)::TEXT IS NULL OR name ILIKE '%' || sqlc.narg(search_query)::TEXT || '%')
|
||||
ORDER BY created_at DESC
|
||||
LIMIT sqlc.arg(page_size) OFFSET sqlc.arg(page_offset);
|
||||
|
||||
-- name: CountImageAssets :one
|
||||
SELECT COUNT(*)::INT AS total
|
||||
FROM image_assets
|
||||
WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
AND (
|
||||
sqlc.narg(folder_id)::BIGINT IS NULL
|
||||
OR (sqlc.narg(folder_id)::BIGINT = 0 AND folder_id IS NULL)
|
||||
OR folder_id = sqlc.narg(folder_id)::BIGINT
|
||||
)
|
||||
AND (sqlc.narg(search_query)::TEXT IS NULL OR name ILIKE '%' || sqlc.narg(search_query)::TEXT || '%');
|
||||
|
||||
-- name: GetImageAsset :one
|
||||
SELECT id, tenant_id, folder_id, object_key, name, size_bytes, status, created_by, created_at, updated_at
|
||||
FROM image_assets
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: InsertImageAsset :one
|
||||
INSERT INTO image_assets (tenant_id, folder_id, object_key, name, size_bytes, status, created_by)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.narg(folder_id), sqlc.arg(object_key), sqlc.arg(name), sqlc.arg(size_bytes), sqlc.arg(status), sqlc.arg(created_by))
|
||||
RETURNING id;
|
||||
|
||||
-- name: UpdateImageAssetStatus :exec
|
||||
UPDATE image_assets
|
||||
SET status = sqlc.arg(status), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: UpdateImageAssetDeleted :exec
|
||||
UPDATE image_assets
|
||||
SET status = 'deleted', deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: UpdateImageAssetName :exec
|
||||
UPDATE image_assets
|
||||
SET name = sqlc.arg(name), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: UpdateImageAssetFolder :exec
|
||||
UPDATE image_assets
|
||||
SET folder_id = sqlc.narg(folder_id), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: ListActiveImagesByFolder :many
|
||||
SELECT id, object_key, size_bytes
|
||||
FROM image_assets
|
||||
WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND folder_id = sqlc.arg(folder_id)
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL;
|
||||
|
||||
-- name: MarkImagesInFolderPendingDelete :exec
|
||||
UPDATE image_assets
|
||||
SET status = 'pending_delete', folder_id = NULL, updated_at = NOW()
|
||||
WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND folder_id = sqlc.arg(folder_id)
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL;
|
||||
|
||||
-- name: CountUncategorizedImages :one
|
||||
SELECT COUNT(*)::INT AS count
|
||||
FROM image_assets
|
||||
WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND folder_id IS NULL
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL;
|
||||
@@ -0,0 +1,35 @@
|
||||
-- name: ListImageFolders :many
|
||||
SELECT f.id, f.tenant_id, f.name, f.created_at, f.updated_at,
|
||||
COUNT(ia.id) FILTER (WHERE ia.status = 'active' AND ia.deleted_at IS NULL)::INT AS image_count
|
||||
FROM image_folders f
|
||||
LEFT JOIN image_assets ia ON ia.tenant_id = f.tenant_id AND ia.folder_id = f.id
|
||||
WHERE f.tenant_id = sqlc.arg(tenant_id)
|
||||
GROUP BY f.id
|
||||
ORDER BY f.name;
|
||||
|
||||
-- name: CreateImageFolder :one
|
||||
INSERT INTO image_folders (tenant_id, name)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.arg(name))
|
||||
RETURNING id;
|
||||
|
||||
-- name: UpdateImageFolder :exec
|
||||
UPDATE image_folders
|
||||
SET name = sqlc.arg(name), updated_at = NOW()
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: GetImageFolder :one
|
||||
SELECT id, tenant_id, name, created_at, updated_at
|
||||
FROM image_folders
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: DeleteImageFolder :exec
|
||||
DELETE FROM image_folders
|
||||
WHERE id = sqlc.arg(id) AND tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: CountActiveImagesInFolder :one
|
||||
SELECT COUNT(*)::INT AS count
|
||||
FROM image_assets
|
||||
WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND folder_id = sqlc.arg(folder_id)
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- name: ListImageReferences :many
|
||||
SELECT r.id, r.tenant_id, r.image_asset_id, r.article_id, r.ref_scope, r.created_at
|
||||
FROM image_asset_references r
|
||||
WHERE r.tenant_id = sqlc.arg(tenant_id) AND r.image_asset_id = sqlc.arg(image_asset_id);
|
||||
|
||||
-- name: CountImageReferences :one
|
||||
SELECT COUNT(*)::INT AS total
|
||||
FROM image_asset_references
|
||||
WHERE tenant_id = sqlc.arg(tenant_id) AND image_asset_id = sqlc.arg(image_asset_id);
|
||||
|
||||
-- name: UpsertImageReference :exec
|
||||
INSERT INTO image_asset_references (tenant_id, image_asset_id, article_id, ref_scope)
|
||||
VALUES (sqlc.arg(tenant_id), sqlc.arg(image_asset_id), sqlc.arg(article_id), sqlc.arg(ref_scope))
|
||||
ON CONFLICT (image_asset_id, article_id, ref_scope) DO UPDATE SET updated_at = NOW();
|
||||
|
||||
-- name: DeleteImageReferencesByArticle :exec
|
||||
DELETE FROM image_asset_references
|
||||
WHERE tenant_id = sqlc.arg(tenant_id) AND article_id = sqlc.arg(article_id);
|
||||
|
||||
-- name: DeleteImageReferencesByArticleScope :exec
|
||||
DELETE FROM image_asset_references
|
||||
WHERE tenant_id = sqlc.arg(tenant_id) AND article_id = sqlc.arg(article_id) AND ref_scope = sqlc.arg(ref_scope);
|
||||
|
||||
-- name: ListImageReferenceArticles :many
|
||||
SELECT DISTINCT r.article_id, r.ref_scope
|
||||
FROM image_asset_references r
|
||||
WHERE r.tenant_id = sqlc.arg(tenant_id) AND r.image_asset_id = sqlc.arg(image_asset_id);
|
||||
@@ -0,0 +1,15 @@
|
||||
-- name: EnsureImageStorageUsageRow :exec
|
||||
INSERT INTO tenant_image_storage_usage (tenant_id, used_bytes)
|
||||
VALUES (sqlc.arg(tenant_id), 0)
|
||||
ON CONFLICT (tenant_id) DO NOTHING;
|
||||
|
||||
-- name: GetImageStorageUsage :one
|
||||
SELECT used_bytes, updated_at
|
||||
FROM tenant_image_storage_usage
|
||||
WHERE tenant_id = sqlc.arg(tenant_id);
|
||||
|
||||
-- name: ReleaseImageStorageQuota :exec
|
||||
UPDATE tenant_image_storage_usage
|
||||
SET used_bytes = GREATEST(0, used_bytes - sqlc.arg(release_bytes)),
|
||||
updated_at = NOW()
|
||||
WHERE tenant_id = sqlc.arg(tenant_id);
|
||||
Reference in New Issue
Block a user