package basemap import ( "context" "database/sql" "errors" "fmt" "path/filepath" "strings" "map-asset-gateway/api-go/internal/uid" ) func (s *Store) EnsureScanSource(ctx context.Context, input CreateScanSourceInput) (ScanSource, error) { code := normalizeBasemapCode(input.Code) if code == "" { return ScanSource{}, errors.New("scan source code is required") } name := strings.TrimSpace(input.Name) if name == "" { name = code } rootPath := strings.TrimSpace(input.RootPath) if rootPath == "" { return ScanSource{}, errors.New("scan source root path is required") } absRoot, err := filepath.Abs(rootPath) if err != nil { return ScanSource{}, fmt.Errorf("resolve root path: %w", err) } manifestName := strings.TrimSpace(input.ManifestName) if manifestName == "" { manifestName = defaultManifestName } now := nowUTC() id := uid.Deterministic("scan-source", code) _, err = s.db.ExecContext(ctx, ` INSERT INTO scan_sources ( id, code, name, root_path, manifest_name, enabled, metadata_json, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, 1, '', ?, ?) ON CONFLICT(code) DO UPDATE SET name = excluded.name, root_path = excluded.root_path, manifest_name = excluded.manifest_name, enabled = 1, updated_at = excluded.updated_at `, id, code, name, absRoot, manifestName, toRFC3339(now), toRFC3339(now)) if err != nil { return ScanSource{}, fmt.Errorf("upsert scan source: %w", err) } return s.GetScanSourceByCode(ctx, code) } func (s *Store) GetScanSourceByCode(ctx context.Context, code string) (ScanSource, error) { row := s.db.QueryRowContext(ctx, ` SELECT id, code, name, root_path, manifest_name, enabled, metadata_json, created_at, updated_at FROM scan_sources WHERE code = ? `, normalizeBasemapCode(code)) item, err := scanScanSource(row) if err != nil { if errors.Is(err, sql.ErrNoRows) { return ScanSource{}, fmt.Errorf("scan source %q not found", code) } return ScanSource{}, err } return item, nil } func (s *Store) ListScanSources(ctx context.Context) ([]ScanSource, error) { rows, err := s.db.QueryContext(ctx, ` SELECT id, code, name, root_path, manifest_name, enabled, metadata_json, created_at, updated_at FROM scan_sources ORDER BY code `) if err != nil { return nil, err } defer rows.Close() var items []ScanSource for rows.Next() { item, err := scanScanSource(rows) if err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } func (s *Store) ListBasemaps(ctx context.Context) ([]Basemap, error) { rows, err := s.db.QueryContext(ctx, ` SELECT id, code, name, type, status, description, created_at, updated_at FROM basemaps ORDER BY code `) if err != nil { return nil, err } defer rows.Close() var items []Basemap indexByID := map[string]int{} for rows.Next() { item, err := scanBasemap(rows) if err != nil { return nil, err } indexByID[item.ID] = len(items) items = append(items, item) } if err := rows.Err(); err != nil { return nil, err } versionRows, err := s.db.QueryContext(ctx, ` SELECT v.id, v.basemap_id, b.code, v.version, v.status, v.is_default, v.manifest_path, v.tile_root_path, v.url_template, v.tile_format, v.tile_scheme, v.min_zoom, v.max_zoom, v.bbox_json, v.attribution, v.metadata_json, v.created_at, v.updated_at FROM basemap_versions v JOIN basemaps b ON b.id = v.basemap_id ORDER BY b.code, v.version `) if err != nil { return nil, err } defer versionRows.Close() for versionRows.Next() { row, err := scanBasemapVersionRow(versionRows) if err != nil { return nil, err } index, ok := indexByID[row.BasemapID] if !ok { continue } version := decodeBasemapVersion(row) items[index].Versions = append(items[index].Versions, version) if version.IsDefault { copyValue := version items[index].Default = ©Value } } if err := versionRows.Err(); err != nil { return nil, err } return items, nil } func (s *Store) GetBasemapByCode(ctx context.Context, code string) (Basemap, error) { items, err := s.ListBasemaps(ctx) if err != nil { return Basemap{}, err } normalized := normalizeBasemapCode(code) for _, item := range items { if item.Code == normalized { return item, nil } } return Basemap{}, fmt.Errorf("basemap %q not found", code) } func (s *Store) SetDefaultVersion(ctx context.Context, basemapCode, version string) error { basemapCode = normalizeBasemapCode(basemapCode) version = strings.TrimSpace(version) if basemapCode == "" || version == "" { return errors.New("basemap code and version are required") } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() var basemapID string if err := tx.QueryRowContext(ctx, `SELECT id FROM basemaps WHERE code = ?`, basemapCode).Scan(&basemapID); err != nil { if errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("basemap %q not found", basemapCode) } return err } result, err := tx.ExecContext(ctx, ` UPDATE basemap_versions SET is_default = CASE WHEN version = ? THEN 1 ELSE 0 END, updated_at = ? WHERE basemap_id = ? `, version, toRFC3339(nowUTC()), basemapID) if err != nil { return err } count, err := result.RowsAffected() if err != nil { return err } if count == 0 { return fmt.Errorf("version %q not found for basemap %q", version, basemapCode) } if _, err := tx.ExecContext(ctx, `UPDATE basemaps SET updated_at = ? WHERE id = ?`, toRFC3339(nowUTC()), basemapID); err != nil { return err } return tx.Commit() } func (s *Store) ListScanRuns(ctx context.Context, limit int) ([]ScanRun, error) { if limit <= 0 { limit = 20 } rows, err := s.db.QueryContext(ctx, ` SELECT r.id, r.scan_source_id, s.code, r.status, r.scanned_count, r.added_count, r.updated_count, r.removed_count, r.summary_json, r.started_at, r.finished_at FROM scan_runs r JOIN scan_sources s ON s.id = r.scan_source_id ORDER BY r.started_at DESC LIMIT ? `, limit) if err != nil { return nil, err } defer rows.Close() var items []ScanRun for rows.Next() { item, err := scanScanRun(rows) if err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } func (s *Store) ListPushRecords(ctx context.Context, limit int) ([]PushRecord, error) { if limit <= 0 { limit = 20 } rows, err := s.db.QueryContext(ctx, ` SELECT p.id, p.target_system_id, t.code, p.basemap_version_id, b.code, v.version, p.status, p.request_json, p.response_status, p.response_body, p.error_message, p.pushed_at, p.finished_at FROM push_records p JOIN target_systems t ON t.id = p.target_system_id JOIN basemap_versions v ON v.id = p.basemap_version_id JOIN basemaps b ON b.id = v.basemap_id ORDER BY p.pushed_at DESC LIMIT ? `, limit) if err != nil { return nil, err } defer rows.Close() var items []PushRecord for rows.Next() { item, err := scanPushRecord(rows) if err != nil { return nil, err } items = append(items, item) } return items, rows.Err() }