推进活动系统最小成品闭环与游客体验
This commit is contained in:
135
backend/internal/store/postgres/asset_store.go
Normal file
135
backend/internal/store/postgres/asset_store.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type ManagedAssetRecord struct {
|
||||
ID string
|
||||
PublicID string
|
||||
AssetType string
|
||||
AssetCode string
|
||||
Version string
|
||||
Title *string
|
||||
SourceMode string
|
||||
StorageProvider string
|
||||
ObjectKey *string
|
||||
PublicURL string
|
||||
FileName *string
|
||||
ContentType *string
|
||||
FileSizeBytes *int64
|
||||
ChecksumSHA256 *string
|
||||
Status string
|
||||
MetadataJSONB map[string]any
|
||||
}
|
||||
|
||||
type CreateManagedAssetParams struct {
|
||||
PublicID string
|
||||
AssetType string
|
||||
AssetCode string
|
||||
Version string
|
||||
Title *string
|
||||
SourceMode string
|
||||
StorageProvider string
|
||||
ObjectKey *string
|
||||
PublicURL string
|
||||
FileName *string
|
||||
ContentType *string
|
||||
FileSizeBytes *int64
|
||||
ChecksumSHA256 *string
|
||||
Status string
|
||||
MetadataJSONB map[string]any
|
||||
}
|
||||
|
||||
func (s *Store) CreateManagedAsset(ctx context.Context, tx pgx.Tx, params CreateManagedAssetParams) (*ManagedAssetRecord, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO managed_assets (
|
||||
asset_public_id, asset_type, asset_code, version, title, source_mode, storage_provider,
|
||||
object_key, public_url, file_name, content_type, file_size_bytes, checksum_sha256, status, metadata_jsonb
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7,
|
||||
$8, $9, $10, $11, $12, $13, $14, COALESCE($15, '{}'::jsonb)
|
||||
)
|
||||
RETURNING id, asset_public_id, asset_type, asset_code, version, title, source_mode, storage_provider,
|
||||
object_key, public_url, file_name, content_type, file_size_bytes, checksum_sha256, status, metadata_jsonb
|
||||
`,
|
||||
params.PublicID, params.AssetType, params.AssetCode, params.Version, params.Title, params.SourceMode, params.StorageProvider,
|
||||
params.ObjectKey, params.PublicURL, params.FileName, params.ContentType, params.FileSizeBytes, params.ChecksumSHA256, params.Status, params.MetadataJSONB,
|
||||
)
|
||||
return scanManagedAsset(row)
|
||||
}
|
||||
|
||||
func (s *Store) ListManagedAssets(ctx context.Context, limit int) ([]ManagedAssetRecord, error) {
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, asset_public_id, asset_type, asset_code, version, title, source_mode, storage_provider,
|
||||
object_key, public_url, file_name, content_type, file_size_bytes, checksum_sha256, status, metadata_jsonb
|
||||
FROM managed_assets
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1
|
||||
`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []ManagedAssetRecord
|
||||
for rows.Next() {
|
||||
record, err := scanManagedAsset(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *record)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) GetManagedAssetByPublicID(ctx context.Context, publicID string) (*ManagedAssetRecord, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT id, asset_public_id, asset_type, asset_code, version, title, source_mode, storage_provider,
|
||||
object_key, public_url, file_name, content_type, file_size_bytes, checksum_sha256, status, metadata_jsonb
|
||||
FROM managed_assets
|
||||
WHERE asset_public_id = $1
|
||||
`, publicID)
|
||||
return scanManagedAsset(row)
|
||||
}
|
||||
|
||||
type managedAssetScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanManagedAsset(scanner managedAssetScanner) (*ManagedAssetRecord, error) {
|
||||
var record ManagedAssetRecord
|
||||
err := scanner.Scan(
|
||||
&record.ID,
|
||||
&record.PublicID,
|
||||
&record.AssetType,
|
||||
&record.AssetCode,
|
||||
&record.Version,
|
||||
&record.Title,
|
||||
&record.SourceMode,
|
||||
&record.StorageProvider,
|
||||
&record.ObjectKey,
|
||||
&record.PublicURL,
|
||||
&record.FileName,
|
||||
&record.ContentType,
|
||||
&record.FileSizeBytes,
|
||||
&record.ChecksumSHA256,
|
||||
&record.Status,
|
||||
&record.MetadataJSONB,
|
||||
)
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if record.MetadataJSONB == nil {
|
||||
record.MetadataJSONB = map[string]any{}
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
@@ -16,6 +16,7 @@ type Card struct {
|
||||
DisplaySlot string
|
||||
DisplayPriority int
|
||||
IsDefaultExperience bool
|
||||
ShowInEventList bool
|
||||
StartsAt *time.Time
|
||||
EndsAt *time.Time
|
||||
EntryChannelID *string
|
||||
@@ -59,6 +60,7 @@ func (s *Store) ListCardsForEntry(ctx context.Context, tenantID string, entryCha
|
||||
c.display_slot,
|
||||
c.display_priority,
|
||||
c.is_default_experience,
|
||||
COALESCE(e.show_in_event_list, true),
|
||||
c.starts_at,
|
||||
c.ends_at,
|
||||
c.entry_channel_id,
|
||||
@@ -117,6 +119,7 @@ func (s *Store) ListCardsForEntry(ctx context.Context, tenantID string, entryCha
|
||||
&card.DisplaySlot,
|
||||
&card.DisplayPriority,
|
||||
&card.IsDefaultExperience,
|
||||
&card.ShowInEventList,
|
||||
&card.StartsAt,
|
||||
&card.EndsAt,
|
||||
&card.EntryChannelID,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,8 @@ type Event struct {
|
||||
DisplayName string
|
||||
Summary *string
|
||||
Status string
|
||||
IsDefaultExperience bool
|
||||
ShowInEventList bool
|
||||
CurrentReleaseID *string
|
||||
CurrentReleasePubID *string
|
||||
ConfigLabel *string
|
||||
@@ -113,6 +115,8 @@ func (s *Store) GetEventByPublicID(ctx context.Context, eventPublicID string) (*
|
||||
e.display_name,
|
||||
e.summary,
|
||||
e.status,
|
||||
e.is_default_experience,
|
||||
e.show_in_event_list,
|
||||
e.current_release_id,
|
||||
er.release_public_id,
|
||||
er.config_label,
|
||||
@@ -159,6 +163,8 @@ func (s *Store) GetEventByPublicID(ctx context.Context, eventPublicID string) (*
|
||||
&event.DisplayName,
|
||||
&event.Summary,
|
||||
&event.Status,
|
||||
&event.IsDefaultExperience,
|
||||
&event.ShowInEventList,
|
||||
&event.CurrentReleaseID,
|
||||
&event.CurrentReleasePubID,
|
||||
&event.ConfigLabel,
|
||||
@@ -202,6 +208,8 @@ func (s *Store) GetEventByID(ctx context.Context, eventID string) (*Event, error
|
||||
e.display_name,
|
||||
e.summary,
|
||||
e.status,
|
||||
e.is_default_experience,
|
||||
e.show_in_event_list,
|
||||
e.current_release_id,
|
||||
er.release_public_id,
|
||||
er.config_label,
|
||||
@@ -248,6 +256,8 @@ func (s *Store) GetEventByID(ctx context.Context, eventID string) (*Event, error
|
||||
&event.DisplayName,
|
||||
&event.Summary,
|
||||
&event.Status,
|
||||
&event.IsDefaultExperience,
|
||||
&event.ShowInEventList,
|
||||
&event.CurrentReleaseID,
|
||||
&event.CurrentReleasePubID,
|
||||
&event.ConfigLabel,
|
||||
@@ -601,3 +611,16 @@ func (s *Store) SetEventReleaseRuntimeBinding(ctx context.Context, tx Tx, releas
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) SetEventReleaseBindings(ctx context.Context, tx Tx, releaseID string, runtimeBindingID, presentationID, contentBundleID *string) error {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE event_releases
|
||||
SET runtime_binding_id = $2,
|
||||
presentation_id = $3,
|
||||
content_bundle_id = $4
|
||||
WHERE id = $1
|
||||
`, releaseID, runtimeBindingID, presentationID, contentBundleID); err != nil {
|
||||
return fmt.Errorf("set event release bindings: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
216
backend/internal/store/postgres/map_experience_store.go
Normal file
216
backend/internal/store/postgres/map_experience_store.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type MapExperienceRow struct {
|
||||
PlacePublicID string
|
||||
PlaceName string
|
||||
MapAssetPublicID string
|
||||
MapAssetName string
|
||||
MapCoverURL *string
|
||||
MapSummary *string
|
||||
TileBaseURL *string
|
||||
TileMetaURL *string
|
||||
EventPublicID *string
|
||||
EventDisplayName *string
|
||||
EventSummary *string
|
||||
EventStatus *string
|
||||
EventIsDefaultExperience bool
|
||||
EventShowInEventList bool
|
||||
EventReleasePayloadJSON *string
|
||||
EventPresentationID *string
|
||||
EventPresentationName *string
|
||||
EventPresentationType *string
|
||||
EventPresentationSchema *string
|
||||
EventContentBundleID *string
|
||||
EventContentBundleName *string
|
||||
EventContentEntryURL *string
|
||||
EventContentAssetRootURL *string
|
||||
EventContentMetadataJSON *string
|
||||
}
|
||||
|
||||
func (s *Store) ListMapExperienceRows(ctx context.Context, limit int) ([]MapExperienceRow, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT
|
||||
p.place_public_id,
|
||||
p.name,
|
||||
ma.map_asset_public_id,
|
||||
ma.name,
|
||||
COALESCE(ma.cover_url, p.cover_url) AS cover_url,
|
||||
COALESCE(ma.description, p.description) AS summary,
|
||||
tr.tile_base_url,
|
||||
tr.meta_url,
|
||||
e.event_public_id,
|
||||
e.display_name,
|
||||
e.summary,
|
||||
e.status,
|
||||
COALESCE(e.is_default_experience, false),
|
||||
COALESCE(e.show_in_event_list, true),
|
||||
er.payload_jsonb::text,
|
||||
ep.presentation_public_id,
|
||||
ep.name,
|
||||
ep.presentation_type,
|
||||
ep.schema_jsonb::text,
|
||||
cb.content_bundle_public_id,
|
||||
cb.name,
|
||||
cb.entry_url,
|
||||
cb.asset_root_url,
|
||||
cb.metadata_jsonb::text
|
||||
FROM map_assets ma
|
||||
JOIN places p ON p.id = ma.place_id
|
||||
LEFT JOIN tile_releases tr ON tr.id = ma.current_tile_release_id
|
||||
LEFT JOIN map_runtime_bindings mrb
|
||||
ON mrb.map_asset_id = ma.id
|
||||
AND mrb.status = 'active'
|
||||
LEFT JOIN event_releases er
|
||||
ON er.runtime_binding_id = mrb.id
|
||||
AND er.status = 'published'
|
||||
LEFT JOIN events e
|
||||
ON e.current_release_id = er.id
|
||||
AND e.status = 'active'
|
||||
LEFT JOIN event_presentations ep ON ep.id = er.presentation_id
|
||||
LEFT JOIN content_bundles cb ON cb.id = er.content_bundle_id
|
||||
WHERE ma.status = 'active'
|
||||
AND (e.id IS NULL OR e.show_in_event_list = true)
|
||||
ORDER BY p.name ASC, ma.name ASC, COALESCE(e.is_default_experience, false) DESC, e.display_name ASC
|
||||
LIMIT $1
|
||||
`, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list map experience rows: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]MapExperienceRow, 0, limit)
|
||||
for rows.Next() {
|
||||
var item MapExperienceRow
|
||||
if err := rows.Scan(
|
||||
&item.PlacePublicID,
|
||||
&item.PlaceName,
|
||||
&item.MapAssetPublicID,
|
||||
&item.MapAssetName,
|
||||
&item.MapCoverURL,
|
||||
&item.MapSummary,
|
||||
&item.TileBaseURL,
|
||||
&item.TileMetaURL,
|
||||
&item.EventPublicID,
|
||||
&item.EventDisplayName,
|
||||
&item.EventSummary,
|
||||
&item.EventStatus,
|
||||
&item.EventIsDefaultExperience,
|
||||
&item.EventShowInEventList,
|
||||
&item.EventReleasePayloadJSON,
|
||||
&item.EventPresentationID,
|
||||
&item.EventPresentationName,
|
||||
&item.EventPresentationType,
|
||||
&item.EventPresentationSchema,
|
||||
&item.EventContentBundleID,
|
||||
&item.EventContentBundleName,
|
||||
&item.EventContentEntryURL,
|
||||
&item.EventContentAssetRootURL,
|
||||
&item.EventContentMetadataJSON,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan map experience row: %w", err)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate map experience rows: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListMapExperienceRowsByMapPublicID(ctx context.Context, mapAssetPublicID string) ([]MapExperienceRow, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT
|
||||
p.place_public_id,
|
||||
p.name,
|
||||
ma.map_asset_public_id,
|
||||
ma.name,
|
||||
COALESCE(ma.cover_url, p.cover_url) AS cover_url,
|
||||
COALESCE(ma.description, p.description) AS summary,
|
||||
tr.tile_base_url,
|
||||
tr.meta_url,
|
||||
e.event_public_id,
|
||||
e.display_name,
|
||||
e.summary,
|
||||
e.status,
|
||||
COALESCE(e.is_default_experience, false),
|
||||
COALESCE(e.show_in_event_list, true),
|
||||
er.payload_jsonb::text,
|
||||
ep.presentation_public_id,
|
||||
ep.name,
|
||||
ep.presentation_type,
|
||||
ep.schema_jsonb::text,
|
||||
cb.content_bundle_public_id,
|
||||
cb.name,
|
||||
cb.entry_url,
|
||||
cb.asset_root_url,
|
||||
cb.metadata_jsonb::text
|
||||
FROM map_assets ma
|
||||
JOIN places p ON p.id = ma.place_id
|
||||
LEFT JOIN tile_releases tr ON tr.id = ma.current_tile_release_id
|
||||
LEFT JOIN map_runtime_bindings mrb
|
||||
ON mrb.map_asset_id = ma.id
|
||||
AND mrb.status = 'active'
|
||||
LEFT JOIN event_releases er
|
||||
ON er.runtime_binding_id = mrb.id
|
||||
AND er.status = 'published'
|
||||
LEFT JOIN events e
|
||||
ON e.current_release_id = er.id
|
||||
AND e.status = 'active'
|
||||
LEFT JOIN event_presentations ep ON ep.id = er.presentation_id
|
||||
LEFT JOIN content_bundles cb ON cb.id = er.content_bundle_id
|
||||
WHERE ma.map_asset_public_id = $1
|
||||
AND ma.status = 'active'
|
||||
AND (e.id IS NULL OR e.show_in_event_list = true)
|
||||
ORDER BY COALESCE(e.is_default_experience, false) DESC, e.display_name ASC
|
||||
`, mapAssetPublicID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list map experience rows by map public id: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]MapExperienceRow, 0, 8)
|
||||
for rows.Next() {
|
||||
var item MapExperienceRow
|
||||
if err := rows.Scan(
|
||||
&item.PlacePublicID,
|
||||
&item.PlaceName,
|
||||
&item.MapAssetPublicID,
|
||||
&item.MapAssetName,
|
||||
&item.MapCoverURL,
|
||||
&item.MapSummary,
|
||||
&item.TileBaseURL,
|
||||
&item.TileMetaURL,
|
||||
&item.EventPublicID,
|
||||
&item.EventDisplayName,
|
||||
&item.EventSummary,
|
||||
&item.EventStatus,
|
||||
&item.EventIsDefaultExperience,
|
||||
&item.EventShowInEventList,
|
||||
&item.EventReleasePayloadJSON,
|
||||
&item.EventPresentationID,
|
||||
&item.EventPresentationName,
|
||||
&item.EventPresentationType,
|
||||
&item.EventPresentationSchema,
|
||||
&item.EventContentBundleID,
|
||||
&item.EventContentBundleName,
|
||||
&item.EventContentEntryURL,
|
||||
&item.EventContentAssetRootURL,
|
||||
&item.EventContentMetadataJSON,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan map experience detail row: %w", err)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate map experience detail rows: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
217
backend/internal/store/postgres/ops_auth_store.go
Normal file
217
backend/internal/store/postgres/ops_auth_store.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type OpsUser struct {
|
||||
ID string
|
||||
PublicID string
|
||||
CountryCode string
|
||||
Mobile string
|
||||
DisplayName string
|
||||
Status string
|
||||
LastLoginAt *time.Time
|
||||
}
|
||||
|
||||
type OpsRole struct {
|
||||
ID string
|
||||
RoleCode string
|
||||
DisplayName string
|
||||
RoleRank int
|
||||
}
|
||||
|
||||
type CreateOpsUserParams struct {
|
||||
PublicID string
|
||||
CountryCode string
|
||||
Mobile string
|
||||
DisplayName string
|
||||
Status string
|
||||
}
|
||||
|
||||
type CreateOpsRefreshTokenParams struct {
|
||||
OpsUserID string
|
||||
DeviceKey string
|
||||
TokenHash string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
type OpsRefreshTokenRecord struct {
|
||||
ID string
|
||||
OpsUserID string
|
||||
DeviceKey *string
|
||||
ExpiresAt time.Time
|
||||
IsRevoked bool
|
||||
}
|
||||
|
||||
func (s *Store) CountOpsUsers(ctx context.Context) (int, error) {
|
||||
row := s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM ops_users WHERE status <> 'deleted'`)
|
||||
var count int
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("count ops users: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetOpsUserByMobile(ctx context.Context, db queryRower, countryCode, mobile string) (*OpsUser, error) {
|
||||
row := db.QueryRow(ctx, `
|
||||
SELECT id, ops_user_public_id, country_code, mobile, display_name, status, last_login_at
|
||||
FROM ops_users
|
||||
WHERE country_code = $1 AND mobile = $2
|
||||
LIMIT 1
|
||||
`, countryCode, mobile)
|
||||
return scanOpsUser(row)
|
||||
}
|
||||
|
||||
func (s *Store) GetOpsUserByID(ctx context.Context, db queryRower, opsUserID string) (*OpsUser, error) {
|
||||
row := db.QueryRow(ctx, `
|
||||
SELECT id, ops_user_public_id, country_code, mobile, display_name, status, last_login_at
|
||||
FROM ops_users
|
||||
WHERE id = $1
|
||||
`, opsUserID)
|
||||
return scanOpsUser(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateOpsUser(ctx context.Context, tx Tx, params CreateOpsUserParams) (*OpsUser, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO ops_users (ops_user_public_id, country_code, mobile, display_name, status)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, ops_user_public_id, country_code, mobile, display_name, status, last_login_at
|
||||
`, params.PublicID, params.CountryCode, params.Mobile, params.DisplayName, params.Status)
|
||||
return scanOpsUser(row)
|
||||
}
|
||||
|
||||
func (s *Store) TouchOpsUserLogin(ctx context.Context, tx Tx, opsUserID string) error {
|
||||
_, err := tx.Exec(ctx, `
|
||||
UPDATE ops_users
|
||||
SET last_login_at = NOW()
|
||||
WHERE id = $1
|
||||
`, opsUserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("touch ops user last login: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) GetOpsRoleByCode(ctx context.Context, tx Tx, roleCode string) (*OpsRole, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
SELECT id, role_code, display_name, role_rank
|
||||
FROM ops_roles
|
||||
WHERE role_code = $1
|
||||
AND status = 'active'
|
||||
LIMIT 1
|
||||
`, roleCode)
|
||||
return scanOpsRole(row)
|
||||
}
|
||||
|
||||
func (s *Store) AssignOpsRole(ctx context.Context, tx Tx, opsUserID, roleID string) error {
|
||||
_, err := tx.Exec(ctx, `
|
||||
INSERT INTO ops_user_roles (ops_user_id, ops_role_id, status)
|
||||
VALUES ($1, $2, 'active')
|
||||
ON CONFLICT (ops_user_id, ops_role_id) DO NOTHING
|
||||
`, opsUserID, roleID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("assign ops role: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) GetPrimaryOpsRole(ctx context.Context, db queryRower, opsUserID string) (*OpsRole, error) {
|
||||
row := db.QueryRow(ctx, `
|
||||
SELECT r.id, r.role_code, r.display_name, r.role_rank
|
||||
FROM ops_user_roles ur
|
||||
JOIN ops_roles r ON r.id = ur.ops_role_id
|
||||
WHERE ur.ops_user_id = $1
|
||||
AND ur.status = 'active'
|
||||
AND r.status = 'active'
|
||||
ORDER BY r.role_rank DESC, r.created_at ASC
|
||||
LIMIT 1
|
||||
`, opsUserID)
|
||||
return scanOpsRole(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateOpsRefreshToken(ctx context.Context, tx Tx, params CreateOpsRefreshTokenParams) (string, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO ops_refresh_tokens (ops_user_id, device_key, token_hash, expires_at)
|
||||
VALUES ($1, NULLIF($2, ''), $3, $4)
|
||||
RETURNING id
|
||||
`, params.OpsUserID, params.DeviceKey, params.TokenHash, params.ExpiresAt)
|
||||
|
||||
var id string
|
||||
if err := row.Scan(&id); err != nil {
|
||||
return "", fmt.Errorf("create ops refresh token: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetOpsRefreshTokenForUpdate(ctx context.Context, tx Tx, tokenHash string) (*OpsRefreshTokenRecord, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
SELECT id, ops_user_id, device_key, expires_at, revoked_at IS NOT NULL
|
||||
FROM ops_refresh_tokens
|
||||
WHERE token_hash = $1
|
||||
FOR UPDATE
|
||||
`, tokenHash)
|
||||
|
||||
var record OpsRefreshTokenRecord
|
||||
err := row.Scan(&record.ID, &record.OpsUserID, &record.DeviceKey, &record.ExpiresAt, &record.IsRevoked)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query ops refresh token for update: %w", err)
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
func (s *Store) RotateOpsRefreshToken(ctx context.Context, tx Tx, oldTokenID, newTokenID string) error {
|
||||
_, err := tx.Exec(ctx, `
|
||||
UPDATE ops_refresh_tokens
|
||||
SET revoked_at = NOW(), replaced_by_token_id = $2
|
||||
WHERE id = $1
|
||||
`, oldTokenID, newTokenID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rotate ops refresh token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) RevokeOpsRefreshToken(ctx context.Context, tokenHash string) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE ops_refresh_tokens
|
||||
SET revoked_at = COALESCE(revoked_at, NOW())
|
||||
WHERE token_hash = $1
|
||||
`, tokenHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("revoke ops refresh token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanOpsUser(row pgx.Row) (*OpsUser, error) {
|
||||
var item OpsUser
|
||||
err := row.Scan(&item.ID, &item.PublicID, &item.CountryCode, &item.Mobile, &item.DisplayName, &item.Status, &item.LastLoginAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan ops user: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanOpsRole(row pgx.Row) (*OpsRole, error) {
|
||||
var item OpsRole
|
||||
err := row.Scan(&item.ID, &item.RoleCode, &item.DisplayName, &item.RoleRank)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan ops role: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
66
backend/internal/store/postgres/ops_summary_store.go
Normal file
66
backend/internal/store/postgres/ops_summary_store.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type OpsOverviewCounts struct {
|
||||
ManagedAssets int
|
||||
Places int
|
||||
MapAssets int
|
||||
TileReleases int
|
||||
CourseSets int
|
||||
CourseVariants int
|
||||
Events int
|
||||
DefaultEvents int
|
||||
PublishedEvents int
|
||||
ConfigSources int
|
||||
Releases int
|
||||
RuntimeBindings int
|
||||
Presentations int
|
||||
ContentBundles int
|
||||
OpsUsers int
|
||||
}
|
||||
|
||||
func (s *Store) GetOpsOverviewCounts(ctx context.Context) (*OpsOverviewCounts, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM managed_assets WHERE status <> 'archived') AS managed_assets,
|
||||
(SELECT COUNT(*) FROM places WHERE status <> 'archived') AS places,
|
||||
(SELECT COUNT(*) FROM map_assets WHERE status <> 'archived') AS map_assets,
|
||||
(SELECT COUNT(*) FROM tile_releases WHERE status <> 'archived') AS tile_releases,
|
||||
(SELECT COUNT(*) FROM course_sets WHERE status <> 'archived') AS course_sets,
|
||||
(SELECT COUNT(*) FROM course_variants WHERE status <> 'archived') AS course_variants,
|
||||
(SELECT COUNT(*) FROM events WHERE status <> 'archived') AS events,
|
||||
(SELECT COUNT(*) FROM events WHERE status <> 'archived' AND COALESCE(is_default_experience, false)) AS default_events,
|
||||
(SELECT COUNT(*) FROM events WHERE status <> 'archived' AND current_release_id IS NOT NULL) AS published_events,
|
||||
(SELECT COUNT(*) FROM event_config_sources) AS config_sources,
|
||||
(SELECT COUNT(*) FROM event_releases) AS releases,
|
||||
(SELECT COUNT(*) FROM map_runtime_bindings WHERE status <> 'archived') AS runtime_bindings,
|
||||
(SELECT COUNT(*) FROM event_presentations WHERE status <> 'archived') AS presentations,
|
||||
(SELECT COUNT(*) FROM content_bundles WHERE status <> 'archived') AS content_bundles,
|
||||
(SELECT COUNT(*) FROM ops_users WHERE status <> 'deleted') AS ops_users
|
||||
`)
|
||||
var item OpsOverviewCounts
|
||||
if err := row.Scan(
|
||||
&item.ManagedAssets,
|
||||
&item.Places,
|
||||
&item.MapAssets,
|
||||
&item.TileReleases,
|
||||
&item.CourseSets,
|
||||
&item.CourseVariants,
|
||||
&item.Events,
|
||||
&item.DefaultEvents,
|
||||
&item.PublishedEvents,
|
||||
&item.ConfigSources,
|
||||
&item.Releases,
|
||||
&item.RuntimeBindings,
|
||||
&item.Presentations,
|
||||
&item.ContentBundles,
|
||||
&item.OpsUsers,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("get ops overview counts: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
@@ -28,6 +28,8 @@ type MapAsset struct {
|
||||
ID string
|
||||
PublicID string
|
||||
PlaceID string
|
||||
PlacePublicID *string
|
||||
PlaceName *string
|
||||
LegacyMapID *string
|
||||
LegacyMapPublicID *string
|
||||
Code string
|
||||
@@ -152,6 +154,16 @@ type CreateMapAssetParams struct {
|
||||
Status string
|
||||
}
|
||||
|
||||
type UpdateMapAssetParams struct {
|
||||
MapAssetID string
|
||||
Code string
|
||||
Name string
|
||||
MapType string
|
||||
CoverURL *string
|
||||
Description *string
|
||||
Status string
|
||||
}
|
||||
|
||||
type CreateTileReleaseParams struct {
|
||||
PublicID string
|
||||
MapAssetID string
|
||||
@@ -215,6 +227,53 @@ type CreateMapRuntimeBindingParams struct {
|
||||
Notes *string
|
||||
}
|
||||
|
||||
type MapAssetLinkedEvent struct {
|
||||
EventPublicID string
|
||||
DisplayName string
|
||||
Summary *string
|
||||
Status string
|
||||
IsDefaultExperience bool
|
||||
ShowInEventList bool
|
||||
CurrentReleasePublicID *string
|
||||
ConfigLabel *string
|
||||
RouteCode *string
|
||||
CurrentPresentationID *string
|
||||
CurrentPresentationName *string
|
||||
CurrentContentBundleID *string
|
||||
CurrentContentBundleName *string
|
||||
}
|
||||
|
||||
func (s *Store) ListMapAssets(ctx context.Context, limit int) ([]MapAsset, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT ma.id, ma.map_asset_public_id, ma.place_id, p.place_public_id, p.name, ma.legacy_map_id, lm.map_public_id, ma.code, ma.name, ma.map_type,
|
||||
ma.cover_url, ma.description, ma.status, ma.current_tile_release_id, ma.created_at, ma.updated_at
|
||||
FROM map_assets ma
|
||||
JOIN places p ON p.id = ma.place_id
|
||||
LEFT JOIN maps lm ON lm.id = ma.legacy_map_id
|
||||
ORDER BY ma.created_at DESC
|
||||
LIMIT $1
|
||||
`, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list all map assets: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []MapAsset{}
|
||||
for rows.Next() {
|
||||
item, err := scanMapAssetFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate all map assets: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListPlaces(ctx context.Context, limit int) ([]Place, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
@@ -253,6 +312,16 @@ func (s *Store) GetPlaceByPublicID(ctx context.Context, publicID string) (*Place
|
||||
return scanPlace(row)
|
||||
}
|
||||
|
||||
func (s *Store) GetPlaceByCode(ctx context.Context, code string) (*Place, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT id, place_public_id, code, name, region, cover_url, description, center_point_jsonb::text, status, created_at, updated_at
|
||||
FROM places
|
||||
WHERE code = $1
|
||||
LIMIT 1
|
||||
`, code)
|
||||
return scanPlace(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreatePlace(ctx context.Context, tx Tx, params CreatePlaceParams) (*Place, error) {
|
||||
centerPointJSON, err := marshalJSONMap(params.CenterPoint)
|
||||
if err != nil {
|
||||
@@ -268,9 +337,10 @@ func (s *Store) CreatePlace(ctx context.Context, tx Tx, params CreatePlaceParams
|
||||
|
||||
func (s *Store) ListMapAssetsByPlaceID(ctx context.Context, placeID string) ([]MapAsset, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT ma.id, ma.map_asset_public_id, ma.place_id, ma.legacy_map_id, lm.map_public_id, ma.code, ma.name, ma.map_type,
|
||||
SELECT ma.id, ma.map_asset_public_id, ma.place_id, p.place_public_id, p.name, ma.legacy_map_id, lm.map_public_id, ma.code, ma.name, ma.map_type,
|
||||
ma.cover_url, ma.description, ma.status, ma.current_tile_release_id, ma.created_at, ma.updated_at
|
||||
FROM map_assets ma
|
||||
JOIN places p ON p.id = ma.place_id
|
||||
LEFT JOIN maps lm ON lm.id = ma.legacy_map_id
|
||||
WHERE ma.place_id = $1
|
||||
ORDER BY ma.created_at DESC
|
||||
@@ -295,9 +365,10 @@ func (s *Store) ListMapAssetsByPlaceID(ctx context.Context, placeID string) ([]M
|
||||
|
||||
func (s *Store) GetMapAssetByPublicID(ctx context.Context, publicID string) (*MapAsset, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT ma.id, ma.map_asset_public_id, ma.place_id, ma.legacy_map_id, lm.map_public_id, ma.code, ma.name, ma.map_type,
|
||||
SELECT ma.id, ma.map_asset_public_id, ma.place_id, p.place_public_id, p.name, ma.legacy_map_id, lm.map_public_id, ma.code, ma.name, ma.map_type,
|
||||
ma.cover_url, ma.description, ma.status, ma.current_tile_release_id, ma.created_at, ma.updated_at
|
||||
FROM map_assets ma
|
||||
JOIN places p ON p.id = ma.place_id
|
||||
LEFT JOIN maps lm ON lm.id = ma.legacy_map_id
|
||||
WHERE ma.map_asset_public_id = $1
|
||||
LIMIT 1
|
||||
@@ -305,15 +376,44 @@ func (s *Store) GetMapAssetByPublicID(ctx context.Context, publicID string) (*Ma
|
||||
return scanMapAsset(row)
|
||||
}
|
||||
|
||||
func (s *Store) GetMapAssetByCode(ctx context.Context, code string) (*MapAsset, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT ma.id, ma.map_asset_public_id, ma.place_id, p.place_public_id, p.name, ma.legacy_map_id, lm.map_public_id, ma.code, ma.name, ma.map_type,
|
||||
ma.cover_url, ma.description, ma.status, ma.current_tile_release_id, ma.created_at, ma.updated_at
|
||||
FROM map_assets ma
|
||||
JOIN places p ON p.id = ma.place_id
|
||||
LEFT JOIN maps lm ON lm.id = ma.legacy_map_id
|
||||
WHERE ma.code = $1
|
||||
LIMIT 1
|
||||
`, code)
|
||||
return scanMapAsset(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateMapAsset(ctx context.Context, tx Tx, params CreateMapAssetParams) (*MapAsset, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO map_assets (map_asset_public_id, place_id, legacy_map_id, code, name, map_type, cover_url, description, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, map_asset_public_id, place_id, legacy_map_id, NULL::text, code, name, map_type, cover_url, description, status, current_tile_release_id, created_at, updated_at
|
||||
RETURNING id, map_asset_public_id, place_id, NULL::text, NULL::text, legacy_map_id, NULL::text, code, name, map_type, cover_url, description, status, current_tile_release_id, created_at, updated_at
|
||||
`, params.PublicID, params.PlaceID, params.LegacyMapID, params.Code, params.Name, params.MapType, params.CoverURL, params.Description, params.Status)
|
||||
return scanMapAsset(row)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateMapAsset(ctx context.Context, tx Tx, params UpdateMapAssetParams) (*MapAsset, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
UPDATE map_assets
|
||||
SET code = $2,
|
||||
name = $3,
|
||||
map_type = $4,
|
||||
cover_url = $5,
|
||||
description = $6,
|
||||
status = $7,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING id, map_asset_public_id, place_id, NULL::text, NULL::text, legacy_map_id, NULL::text, code, name, map_type, cover_url, description, status, current_tile_release_id, created_at, updated_at
|
||||
`, params.MapAssetID, params.Code, params.Name, params.MapType, params.CoverURL, params.Description, params.Status)
|
||||
return scanMapAsset(row)
|
||||
}
|
||||
|
||||
func (s *Store) ListTileReleasesByMapAssetID(ctx context.Context, mapAssetID string) ([]TileRelease, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT tr.id, tr.tile_release_public_id, tr.map_asset_id, tr.legacy_map_version_id, mv.version_public_id,
|
||||
@@ -355,6 +455,19 @@ func (s *Store) GetTileReleaseByPublicID(ctx context.Context, publicID string) (
|
||||
return scanTileRelease(row)
|
||||
}
|
||||
|
||||
func (s *Store) GetTileReleaseByMapAssetIDAndVersionCode(ctx context.Context, mapAssetID, versionCode string) (*TileRelease, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT tr.id, tr.tile_release_public_id, tr.map_asset_id, tr.legacy_map_version_id, mv.version_public_id,
|
||||
tr.version_code, tr.status, tr.tile_base_url, tr.meta_url, tr.published_asset_root,
|
||||
tr.metadata_jsonb::text, tr.published_at, tr.created_at, tr.updated_at
|
||||
FROM tile_releases tr
|
||||
LEFT JOIN map_versions mv ON mv.id = tr.legacy_map_version_id
|
||||
WHERE tr.map_asset_id = $1 AND tr.version_code = $2
|
||||
LIMIT 1
|
||||
`, mapAssetID, versionCode)
|
||||
return scanTileRelease(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateTileRelease(ctx context.Context, tx Tx, params CreateTileReleaseParams) (*TileRelease, error) {
|
||||
metadataJSON, err := marshalJSONMap(params.MetadataJSON)
|
||||
if err != nil {
|
||||
@@ -506,6 +619,16 @@ func (s *Store) GetCourseSetByPublicID(ctx context.Context, publicID string) (*C
|
||||
return scanCourseSet(row)
|
||||
}
|
||||
|
||||
func (s *Store) GetCourseSetByCode(ctx context.Context, code string) (*CourseSet, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT id, course_set_public_id, place_id, map_asset_id, code, mode, name, description, status, current_variant_id, created_at, updated_at
|
||||
FROM course_sets
|
||||
WHERE code = $1
|
||||
LIMIT 1
|
||||
`, code)
|
||||
return scanCourseSet(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateCourseSet(ctx context.Context, tx Tx, params CreateCourseSetParams) (*CourseSet, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO course_sets (course_set_public_id, place_id, map_asset_id, code, mode, name, description, status)
|
||||
@@ -556,6 +679,19 @@ func (s *Store) GetCourseVariantByPublicID(ctx context.Context, publicID string)
|
||||
return scanCourseVariant(row)
|
||||
}
|
||||
|
||||
func (s *Store) GetCourseVariantByCourseSetIDAndRouteCode(ctx context.Context, courseSetID, routeCode string) (*CourseVariant, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT cv.id, cv.course_variant_public_id, cv.course_set_id, cv.source_id, cs.course_source_public_id, cv.name, cv.route_code,
|
||||
cv.mode, cv.control_count, cv.difficulty, cv.status, cv.is_default,
|
||||
cv.config_patch_jsonb::text, cv.metadata_jsonb::text, cv.created_at, cv.updated_at
|
||||
FROM course_variants cv
|
||||
LEFT JOIN course_sources cs ON cs.id = cv.source_id
|
||||
WHERE cv.course_set_id = $1 AND cv.route_code = $2
|
||||
LIMIT 1
|
||||
`, courseSetID, routeCode)
|
||||
return scanCourseVariant(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateCourseVariant(ctx context.Context, tx Tx, params CreateCourseVariantParams) (*CourseVariant, error) {
|
||||
configPatchJSON, err := marshalJSONMap(params.ConfigPatch)
|
||||
if err != nil {
|
||||
@@ -641,6 +777,66 @@ func (s *Store) GetMapRuntimeBindingByPublicID(ctx context.Context, publicID str
|
||||
return scanMapRuntimeBinding(row)
|
||||
}
|
||||
|
||||
func (s *Store) ListMapAssetLinkedEvents(ctx context.Context, mapAssetID string, limit int) ([]MapAssetLinkedEvent, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT
|
||||
e.event_public_id,
|
||||
e.display_name,
|
||||
e.summary,
|
||||
e.status,
|
||||
COALESCE(e.is_default_experience, false),
|
||||
COALESCE(e.show_in_event_list, true),
|
||||
er.release_public_id,
|
||||
er.config_label,
|
||||
er.route_code,
|
||||
ep.presentation_public_id,
|
||||
ep.name,
|
||||
cb.content_bundle_public_id,
|
||||
cb.name
|
||||
FROM events e
|
||||
JOIN map_runtime_bindings mrb ON mrb.id = e.current_runtime_binding_id
|
||||
LEFT JOIN event_releases er ON er.id = e.current_release_id
|
||||
LEFT JOIN event_presentations ep ON ep.id = e.current_presentation_id
|
||||
LEFT JOIN content_bundles cb ON cb.id = e.current_content_bundle_id
|
||||
WHERE mrb.map_asset_id = $1
|
||||
ORDER BY COALESCE(e.is_default_experience, false) DESC, e.display_name ASC
|
||||
LIMIT $2
|
||||
`, mapAssetID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list map asset linked events: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []MapAssetLinkedEvent{}
|
||||
for rows.Next() {
|
||||
var item MapAssetLinkedEvent
|
||||
if err := rows.Scan(
|
||||
&item.EventPublicID,
|
||||
&item.DisplayName,
|
||||
&item.Summary,
|
||||
&item.Status,
|
||||
&item.IsDefaultExperience,
|
||||
&item.ShowInEventList,
|
||||
&item.CurrentReleasePublicID,
|
||||
&item.ConfigLabel,
|
||||
&item.RouteCode,
|
||||
&item.CurrentPresentationID,
|
||||
&item.CurrentPresentationName,
|
||||
&item.CurrentContentBundleID,
|
||||
&item.CurrentContentBundleName,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan map asset linked event: %w", err)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate map asset linked events: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateMapRuntimeBinding(ctx context.Context, tx Tx, params CreateMapRuntimeBindingParams) (*MapRuntimeBinding, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO map_runtime_bindings (
|
||||
@@ -681,7 +877,7 @@ func scanPlaceFromRows(rows pgx.Rows) (*Place, error) {
|
||||
|
||||
func scanMapAsset(row pgx.Row) (*MapAsset, error) {
|
||||
var item MapAsset
|
||||
err := row.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.LegacyMapID, &item.LegacyMapPublicID, &item.Code, &item.Name, &item.MapType, &item.CoverURL, &item.Description, &item.Status, &item.CurrentTileReleaseID, &item.CreatedAt, &item.UpdatedAt)
|
||||
err := row.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.PlacePublicID, &item.PlaceName, &item.LegacyMapID, &item.LegacyMapPublicID, &item.Code, &item.Name, &item.MapType, &item.CoverURL, &item.Description, &item.Status, &item.CurrentTileReleaseID, &item.CreatedAt, &item.UpdatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -693,7 +889,7 @@ func scanMapAsset(row pgx.Row) (*MapAsset, error) {
|
||||
|
||||
func scanMapAssetFromRows(rows pgx.Rows) (*MapAsset, error) {
|
||||
var item MapAsset
|
||||
err := rows.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.LegacyMapID, &item.LegacyMapPublicID, &item.Code, &item.Name, &item.MapType, &item.CoverURL, &item.Description, &item.Status, &item.CurrentTileReleaseID, &item.CreatedAt, &item.UpdatedAt)
|
||||
err := rows.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.PlacePublicID, &item.PlaceName, &item.LegacyMapID, &item.LegacyMapPublicID, &item.Code, &item.Name, &item.MapType, &item.CoverURL, &item.Description, &item.Status, &item.CurrentTileReleaseID, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan map asset row: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user