113 lines
3.1 KiB
Go
113 lines
3.1 KiB
Go
package feeds
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"github.com/jackc/pgx/v5"
|
||
|
|
)
|
||
|
|
|
||
|
|
// requiredStandardField is an enabled, required company standard field used for sync preflight.
|
||
|
|
type requiredStandardField struct {
|
||
|
|
Key string
|
||
|
|
Name string
|
||
|
|
}
|
||
|
|
|
||
|
|
// activeMappings returns rows with a non-empty source and a real target (not "none").
|
||
|
|
// Mirrors the frontend activeMappingRows filter.
|
||
|
|
func activeMappings(mappings []FieldMapping) []FieldMapping {
|
||
|
|
out := make([]FieldMapping, 0, len(mappings))
|
||
|
|
for _, m := range mappings {
|
||
|
|
src := m.sourceKey()
|
||
|
|
tgt := m.targetKey()
|
||
|
|
if src == "" || tgt == "" || strings.EqualFold(tgt, "none") {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
out = append(out, m)
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
// validateMappingsForSync returns a ClientMsg when mappings are empty or required targets are missing.
|
||
|
|
// Optional (non-required) standard fields may remain unmapped.
|
||
|
|
func validateMappingsForSync(mappings []FieldMapping, required []requiredStandardField) error {
|
||
|
|
active := activeMappings(mappings)
|
||
|
|
if len(active) == 0 {
|
||
|
|
return ClientMsg("Map at least one source field before syncing.")
|
||
|
|
}
|
||
|
|
if len(required) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
mapped := make(map[string]struct{}, len(active))
|
||
|
|
for _, m := range active {
|
||
|
|
mapped[m.targetKey()] = struct{}{}
|
||
|
|
}
|
||
|
|
|
||
|
|
missing := make([]string, 0)
|
||
|
|
for _, r := range required {
|
||
|
|
if _, ok := mapped[r.Key]; ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
label := strings.TrimSpace(r.Name)
|
||
|
|
if label == "" {
|
||
|
|
label = r.Key
|
||
|
|
}
|
||
|
|
missing = append(missing, label)
|
||
|
|
}
|
||
|
|
if len(missing) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return ClientMsg(fmt.Sprintf("Map required fields before syncing: %s.", strings.Join(missing, ", ")))
|
||
|
|
}
|
||
|
|
|
||
|
|
// loadRequiredStandardFields returns enabled+required standard field keys for the company.
|
||
|
|
func (s *Service) loadRequiredStandardFields(ctx context.Context, companyID uuid.UUID) ([]requiredStandardField, error) {
|
||
|
|
if s == nil || s.Pool == nil {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
rows, err := s.Pool.Query(ctx, `
|
||
|
|
SELECT key, COALESCE(NULLIF(TRIM(name), ''), key) AS name
|
||
|
|
FROM standard_fields
|
||
|
|
WHERE company_id = $1 AND enabled = true AND is_required = true
|
||
|
|
ORDER BY sort_order ASC, key ASC`, companyID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
|
||
|
|
out := make([]requiredStandardField, 0)
|
||
|
|
for rows.Next() {
|
||
|
|
var key, name string
|
||
|
|
if err := rows.Scan(&key, &name); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
key = strings.TrimSpace(key)
|
||
|
|
if key == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
out = append(out, requiredStandardField{Key: key, Name: strings.TrimSpace(name)})
|
||
|
|
}
|
||
|
|
return out, rows.Err()
|
||
|
|
}
|
||
|
|
|
||
|
|
// ensureMappingsReadyForSync loads active mappings and required standard fields, then validates.
|
||
|
|
// Called before creating a sync job so clients get a clear 400 without a failed job row.
|
||
|
|
func (s *Service) ensureMappingsReadyForSync(ctx context.Context, companyID, feedID uuid.UUID) error {
|
||
|
|
raw, err := s.loadMappingsRaw(ctx, companyID, feedID)
|
||
|
|
if err != nil {
|
||
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
||
|
|
return ClientMsg("Map at least one source field before syncing.")
|
||
|
|
}
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
required, err := s.loadRequiredStandardFields(ctx, companyID)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
return validateMappingsForSync(parseMappings(raw), required)
|
||
|
|
}
|