|
- package sqlite
-
- import (
- "context"
- "database/sql"
- "embed"
- "encoding/json"
- "fmt"
- "io/fs"
- "os"
- "path/filepath"
- "sort"
- "strings"
- "time"
-
- _ "modernc.org/sqlite"
-
- "qctextbuilder/internal/domain"
- "qctextbuilder/internal/store"
- )
-
- //go:embed migrations/*.sql
- var migrationFS embed.FS
-
- type Store struct {
- db *sql.DB
- }
-
- func New(dbPath string) (*Store, error) {
- path := strings.TrimSpace(dbPath)
- if path == "" {
- path = "data/qctextbuilder.db"
- }
- if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
- return nil, fmt.Errorf("create db directory: %w", err)
- }
-
- db, err := sql.Open("sqlite", path)
- if err != nil {
- return nil, fmt.Errorf("open sqlite: %w", err)
- }
- db.SetMaxOpenConns(1)
- if _, err := db.Exec("PRAGMA foreign_keys = ON;"); err != nil {
- _ = db.Close()
- return nil, fmt.Errorf("enable foreign keys: %w", err)
- }
- if err := runMigrations(db); err != nil {
- _ = db.Close()
- return nil, fmt.Errorf("run migrations: %w", err)
- }
- return &Store{db: db}, nil
- }
-
- func (s *Store) Close() error {
- if s == nil || s.db == nil {
- return nil
- }
- return s.db.Close()
- }
-
- func (s *Store) UpsertTemplates(ctx context.Context, templates []domain.Template) error {
- tx, err := s.db.BeginTx(ctx, nil)
- if err != nil {
- return err
- }
- defer rollback(tx)
-
- stmt := `
- INSERT INTO qc_templates (
- id, name, description, locale, thumbnail_url, template_preview_url, type,
- palette_ready, raw_template_json, is_ai_template, is_onboarded, manifest_status, last_discovered_at, updated_at
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT is_onboarded FROM qc_templates WHERE id = ?), ?),
- COALESCE((SELECT manifest_status FROM qc_templates WHERE id = ?), ?),
- COALESCE((SELECT last_discovered_at FROM qc_templates WHERE id = ?), ?),
- ?
- )
- ON CONFLICT(id) DO UPDATE SET
- name = excluded.name,
- description = excluded.description,
- locale = excluded.locale,
- thumbnail_url = excluded.thumbnail_url,
- template_preview_url = excluded.template_preview_url,
- type = excluded.type,
- palette_ready = excluded.palette_ready,
- raw_template_json = excluded.raw_template_json,
- is_ai_template = excluded.is_ai_template,
- updated_at = excluded.updated_at;
- `
- now := time.Now().UTC()
- for _, t := range templates {
- _, err := tx.ExecContext(ctx, stmt,
- t.ID, t.Name, t.Description, t.Locale, t.ThumbnailURL, t.TemplatePreviewURL, t.Type,
- boolToInt(t.PaletteReady), asRaw(t.RawJSON), boolToInt(t.IsAITemplate),
- t.ID, boolToInt(t.IsOnboarded),
- t.ID, defaultString(t.ManifestStatus, "missing"),
- t.ID, asRFC3339Ptr(t.LastDiscoveredAt),
- now.Format(time.RFC3339Nano),
- )
- if err != nil {
- return fmt.Errorf("upsert template %d: %w", t.ID, err)
- }
- }
- return tx.Commit()
- }
-
- func (s *Store) GetTemplateByID(ctx context.Context, id int64) (*domain.Template, error) {
- row := s.db.QueryRowContext(ctx, `
- SELECT id, name, description, locale, thumbnail_url, template_preview_url, type,
- palette_ready, raw_template_json, is_ai_template, is_onboarded, manifest_status, last_discovered_at
- FROM qc_templates
- WHERE id = ?`, id)
- t, err := scanTemplate(row.Scan)
- if err != nil {
- return nil, err
- }
- return t, nil
- }
-
- func (s *Store) ListTemplates(ctx context.Context) ([]domain.Template, error) {
- rows, err := s.db.QueryContext(ctx, `
- SELECT id, name, description, locale, thumbnail_url, template_preview_url, type,
- palette_ready, raw_template_json, is_ai_template, is_onboarded, manifest_status, last_discovered_at
- FROM qc_templates`)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- out := make([]domain.Template, 0)
- for rows.Next() {
- t, err := scanTemplate(rows.Scan)
- if err != nil {
- return nil, err
- }
- out = append(out, *t)
- }
- return out, rows.Err()
- }
-
- func (s *Store) SetTemplateManifestStatus(ctx context.Context, templateID int64, status string, onboarded bool) error {
- res, err := s.db.ExecContext(ctx, `
- UPDATE qc_templates
- SET manifest_status = ?, is_onboarded = ?, last_discovered_at = ?, updated_at = ?
- WHERE id = ?`,
- defaultString(status, "missing"),
- boolToInt(onboarded),
- time.Now().UTC().Format(time.RFC3339Nano),
- time.Now().UTC().Format(time.RFC3339Nano),
- templateID,
- )
- if err != nil {
- return err
- }
- n, _ := res.RowsAffected()
- if n == 0 {
- return store.ErrNotFound
- }
- return nil
- }
-
- func (s *Store) CreateManifest(ctx context.Context, manifest domain.TemplateManifest, fields []domain.TemplateField) error {
- tx, err := s.db.BeginTx(ctx, nil)
- if err != nil {
- return err
- }
- defer rollback(tx)
-
- if _, err := tx.ExecContext(ctx, `UPDATE qc_template_manifests SET is_active = 0 WHERE template_id = ?`, manifest.TemplateID); err != nil {
- return err
- }
-
- _, err = tx.ExecContext(ctx, `
- INSERT INTO qc_template_manifests (
- id, template_id, manifest_version, source, language_used_discovery, discovery_payload_json,
- discovery_response_json, flattened_manifest_json, is_active, created_at, updated_at
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- manifest.ID, manifest.TemplateID, manifest.Version, manifest.Source, manifest.LanguageUsedDiscovery,
- asRaw(manifest.DiscoveryPayloadJSON), asRaw(manifest.DiscoveryResponseJSON), asRaw(manifest.FlattenedManifestJSON),
- boolToInt(manifest.IsActive), manifest.CreatedAt.UTC().Format(time.RFC3339Nano), manifest.UpdatedAt.UTC().Format(time.RFC3339Nano),
- )
- if err != nil {
- return err
- }
-
- if _, err := tx.ExecContext(ctx, `DELETE FROM qc_template_fields WHERE manifest_id = ?`, manifest.ID); err != nil {
- return err
- }
- for _, f := range fields {
- _, err := tx.ExecContext(ctx, `
- INSERT INTO qc_template_fields (
- id, template_id, manifest_id, section, website_section, key_name, path, field_kind,
- sample_value, is_enabled, is_required_by_us, display_label, display_order, notes
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- f.ID, f.TemplateID, f.ManifestID, f.Section, domain.NormalizeWebsiteSection(f.WebsiteSection), f.KeyName, f.Path, f.FieldKind,
- f.SampleValue, boolToInt(f.IsEnabled), boolToInt(f.IsRequiredByUs), f.DisplayLabel, f.DisplayOrder, f.Notes,
- )
- if err != nil {
- return err
- }
- }
- return tx.Commit()
- }
-
- func (s *Store) GetActiveManifestByTemplateID(ctx context.Context, templateID int64) (*domain.TemplateManifest, error) {
- row := s.db.QueryRowContext(ctx, `
- SELECT id, template_id, manifest_version, source, language_used_discovery, discovery_payload_json,
- discovery_response_json, flattened_manifest_json, is_active, created_at, updated_at
- FROM qc_template_manifests
- WHERE template_id = ? AND is_active = 1
- ORDER BY created_at DESC
- LIMIT 1`, templateID)
- manifest, err := scanManifest(row.Scan)
- if err != nil {
- return nil, err
- }
- return manifest, nil
- }
-
- func (s *Store) ListFieldsByManifestID(ctx context.Context, manifestID string) ([]domain.TemplateField, error) {
- rows, err := s.db.QueryContext(ctx, `
- SELECT id, template_id, manifest_id, section, website_section, key_name, path, field_kind, sample_value,
- is_enabled, is_required_by_us, display_label, display_order, notes
- FROM qc_template_fields
- WHERE manifest_id = ?
- ORDER BY display_order ASC, id ASC`, manifestID)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- fields := make([]domain.TemplateField, 0)
- for rows.Next() {
- var f domain.TemplateField
- var isEnabled, isRequired int
- if err := rows.Scan(
- &f.ID, &f.TemplateID, &f.ManifestID, &f.Section, &f.WebsiteSection, &f.KeyName, &f.Path, &f.FieldKind, &f.SampleValue,
- &isEnabled, &isRequired, &f.DisplayLabel, &f.DisplayOrder, &f.Notes,
- ); err != nil {
- return nil, err
- }
- f.IsEnabled = isEnabled == 1
- f.IsRequiredByUs = isRequired == 1
- f.WebsiteSection = domain.NormalizeWebsiteSection(f.WebsiteSection)
- fields = append(fields, f)
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- if len(fields) == 0 {
- return nil, store.ErrNotFound
- }
- return fields, nil
- }
-
- func (s *Store) UpdateFields(ctx context.Context, manifestID string, fields []domain.TemplateField) error {
- tx, err := s.db.BeginTx(ctx, nil)
- if err != nil {
- return err
- }
- defer rollback(tx)
-
- var exists int
- if err := tx.QueryRowContext(ctx, `SELECT COUNT(1) FROM qc_template_manifests WHERE id = ?`, manifestID).Scan(&exists); err != nil {
- return err
- }
- if exists == 0 {
- return store.ErrNotFound
- }
- if _, err := tx.ExecContext(ctx, `DELETE FROM qc_template_fields WHERE manifest_id = ?`, manifestID); err != nil {
- return err
- }
- for _, f := range fields {
- _, err := tx.ExecContext(ctx, `
- INSERT INTO qc_template_fields (
- id, template_id, manifest_id, section, website_section, key_name, path, field_kind,
- sample_value, is_enabled, is_required_by_us, display_label, display_order, notes
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- f.ID, f.TemplateID, f.ManifestID, f.Section, domain.NormalizeWebsiteSection(f.WebsiteSection), f.KeyName, f.Path, f.FieldKind,
- f.SampleValue, boolToInt(f.IsEnabled), boolToInt(f.IsRequiredByUs), f.DisplayLabel, f.DisplayOrder, f.Notes,
- )
- if err != nil {
- return err
- }
- }
- return tx.Commit()
- }
-
- func (s *Store) CreateBuild(ctx context.Context, build domain.SiteBuild) error {
- _, err := s.db.ExecContext(ctx, `
- INSERT INTO site_builds (
- id, template_id, manifest_id, request_name, global_data_json, ai_data_json, final_sites_payload_json,
- qc_job_id, qc_site_id, qc_status, qc_preview_url, qc_editor_url, qc_result_json, qc_error_json, started_at, finished_at
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- build.ID, build.TemplateID, build.ManifestID, build.RequestName, asRaw(build.GlobalDataJSON), asRaw(build.AIDataJSON), asRaw(build.FinalSitesPayload),
- build.QCJobID, build.QCSiteID, build.QCStatus, build.QCPreviewURL, build.QCEditorURL, asRaw(build.QCResultJSON), asRaw(build.QCErrorJSON),
- asRFC3339Ptr(build.StartedAt), asRFC3339Ptr(build.FinishedAt),
- )
- return err
- }
-
- func (s *Store) GetBuildByID(ctx context.Context, id string) (*domain.SiteBuild, error) {
- row := s.db.QueryRowContext(ctx, `
- SELECT id, template_id, manifest_id, request_name, global_data_json, ai_data_json, final_sites_payload_json,
- qc_job_id, qc_site_id, qc_status, qc_preview_url, qc_editor_url, qc_result_json, qc_error_json, started_at, finished_at
- FROM site_builds
- WHERE id = ?`, id)
- build, err := scanBuild(row.Scan)
- if err != nil {
- return nil, err
- }
- return build, nil
- }
-
- func (s *Store) ListBuildsByStatuses(ctx context.Context, statuses []string, limit int) ([]domain.SiteBuild, error) {
- base := `
- SELECT id, template_id, manifest_id, request_name, global_data_json, ai_data_json, final_sites_payload_json,
- qc_job_id, qc_site_id, qc_status, qc_preview_url, qc_editor_url, qc_result_json, qc_error_json, started_at, finished_at
- FROM site_builds`
-
- args := make([]any, 0)
- parts := make([]string, 0)
- if len(statuses) > 0 {
- placeholders := make([]string, 0, len(statuses))
- for _, status := range statuses {
- placeholders = append(placeholders, "?")
- args = append(args, status)
- }
- parts = append(parts, "qc_status IN ("+strings.Join(placeholders, ", ")+")")
- }
- query := base
- if len(parts) > 0 {
- query += " WHERE " + strings.Join(parts, " AND ")
- }
- query += " ORDER BY started_at ASC, id ASC"
- if limit > 0 {
- query += " LIMIT ?"
- args = append(args, limit)
- }
-
- rows, err := s.db.QueryContext(ctx, query, args...)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- builds := make([]domain.SiteBuild, 0)
- for rows.Next() {
- build, err := scanBuild(rows.Scan)
- if err != nil {
- return nil, err
- }
- builds = append(builds, *build)
- }
- return builds, rows.Err()
- }
-
- func (s *Store) MarkBuildSubmitted(ctx context.Context, buildID string, jobID int64, status string, qcResult json.RawMessage, startedAt time.Time) error {
- res, err := s.db.ExecContext(ctx, `
- UPDATE site_builds
- SET qc_job_id = ?, qc_status = ?, qc_result_json = ?, started_at = ?
- WHERE id = ?`,
- jobID, status, asRaw(qcResult), startedAt.UTC().Format(time.RFC3339Nano), buildID,
- )
- if err != nil {
- return err
- }
- n, _ := res.RowsAffected()
- if n == 0 {
- return store.ErrNotFound
- }
- return nil
- }
-
- func (s *Store) UpdateBuildFromJob(ctx context.Context, buildID string, status string, siteID *int64, previewURL string, qcResult json.RawMessage, qcError json.RawMessage, finishedAt *time.Time) error {
- res, err := s.db.ExecContext(ctx, `
- UPDATE site_builds
- SET qc_status = ?, qc_site_id = COALESCE(?, qc_site_id), qc_preview_url = ?, qc_result_json = ?, qc_error_json = ?, finished_at = ?
- WHERE id = ?`,
- status, siteID, previewURL, asRaw(qcResult), asRaw(qcError), asRFC3339Ptr(finishedAt), buildID,
- )
- if err != nil {
- return err
- }
- n, _ := res.RowsAffected()
- if n == 0 {
- return store.ErrNotFound
- }
- return nil
- }
-
- func (s *Store) UpdateBuildEditorURL(ctx context.Context, buildID string, editorURL string, qcResult json.RawMessage) error {
- res, err := s.db.ExecContext(ctx, `
- UPDATE site_builds
- SET qc_editor_url = ?, qc_result_json = ?
- WHERE id = ?`,
- editorURL, asRaw(qcResult), buildID,
- )
- if err != nil {
- return err
- }
- n, _ := res.RowsAffected()
- if n == 0 {
- return store.ErrNotFound
- }
- return nil
- }
-
- func (s *Store) UpsertSettings(ctx context.Context, settings domain.AppSettings) error {
- promptBlocksRaw, err := json.Marshal(domain.NormalizePromptBlocks(settings.PromptBlocks))
- if err != nil {
- return fmt.Errorf("marshal prompt blocks: %w", err)
- }
- _, err = s.db.ExecContext(ctx, `
- INSERT INTO app_settings (
- id, qc_base_url, qc_bearer_token_encrypted, language_output_mode, job_poll_interval_seconds, job_poll_timeout_seconds, master_prompt, prompt_blocks_json, updated_at
- ) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)
- ON CONFLICT(id) DO UPDATE SET
- qc_base_url = excluded.qc_base_url,
- qc_bearer_token_encrypted = excluded.qc_bearer_token_encrypted,
- language_output_mode = excluded.language_output_mode,
- job_poll_interval_seconds = excluded.job_poll_interval_seconds,
- job_poll_timeout_seconds = excluded.job_poll_timeout_seconds,
- master_prompt = excluded.master_prompt,
- prompt_blocks_json = excluded.prompt_blocks_json,
- updated_at = excluded.updated_at`,
- settings.QCBaseURL,
- settings.QCBearerTokenEncrypted,
- defaultString(settings.LanguageOutputMode, "EN"),
- settings.JobPollIntervalSeconds,
- settings.JobPollTimeoutSeconds,
- domain.NormalizeMasterPrompt(settings.MasterPrompt),
- promptBlocksRaw,
- time.Now().UTC().Format(time.RFC3339Nano),
- )
- return err
- }
-
- func (s *Store) GetSettings(ctx context.Context) (*domain.AppSettings, error) {
- row := s.db.QueryRowContext(ctx, `
- SELECT qc_base_url, qc_bearer_token_encrypted, language_output_mode, job_poll_interval_seconds, job_poll_timeout_seconds, master_prompt, prompt_blocks_json
- FROM app_settings
- WHERE id = 1`)
- var settings domain.AppSettings
- var promptBlocksRaw []byte
- if err := row.Scan(
- &settings.QCBaseURL,
- &settings.QCBearerTokenEncrypted,
- &settings.LanguageOutputMode,
- &settings.JobPollIntervalSeconds,
- &settings.JobPollTimeoutSeconds,
- &settings.MasterPrompt,
- &promptBlocksRaw,
- ); err != nil {
- if err == sql.ErrNoRows {
- return nil, store.ErrNotFound
- }
- return nil, err
- }
- settings.MasterPrompt = domain.NormalizeMasterPrompt(settings.MasterPrompt)
- if len(promptBlocksRaw) > 0 {
- _ = json.Unmarshal(promptBlocksRaw, &settings.PromptBlocks)
- }
- settings.PromptBlocks = domain.NormalizePromptBlocks(settings.PromptBlocks)
- return &settings, nil
- }
-
- func (s *Store) CreateDraft(ctx context.Context, draft domain.BuildDraft) error {
- _, err := s.db.ExecContext(ctx, `
- INSERT INTO build_drafts (
- id, template_id, manifest_id, source, request_name, global_data_json,
- field_values_json, draft_context_json, status, notes, created_at, updated_at
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- draft.ID, nullableInt64(draft.TemplateID), draft.ManifestID, draft.Source, draft.RequestName, asRaw(draft.GlobalDataJSON),
- asRaw(draft.FieldValuesJSON), asRaw(draft.DraftContextJSON), draft.Status, draft.Notes, draft.CreatedAt.UTC().Format(time.RFC3339Nano), draft.UpdatedAt.UTC().Format(time.RFC3339Nano),
- )
- return err
- }
-
- func (s *Store) UpdateDraft(ctx context.Context, draft domain.BuildDraft) error {
- res, err := s.db.ExecContext(ctx, `
- UPDATE build_drafts
- SET template_id = ?, manifest_id = ?, source = ?, request_name = ?, global_data_json = ?, field_values_json = ?, draft_context_json = ?,
- status = ?, notes = ?, updated_at = ?
- WHERE id = ?`,
- nullableInt64(draft.TemplateID), draft.ManifestID, draft.Source, draft.RequestName, asRaw(draft.GlobalDataJSON), asRaw(draft.FieldValuesJSON), asRaw(draft.DraftContextJSON),
- draft.Status, draft.Notes, draft.UpdatedAt.UTC().Format(time.RFC3339Nano), draft.ID,
- )
- if err != nil {
- return err
- }
- n, _ := res.RowsAffected()
- if n == 0 {
- return store.ErrNotFound
- }
- return nil
- }
-
- func (s *Store) GetDraftByID(ctx context.Context, id string) (*domain.BuildDraft, error) {
- row := s.db.QueryRowContext(ctx, `
- SELECT id, template_id, manifest_id, source, request_name, global_data_json, field_values_json, draft_context_json, status, notes, created_at, updated_at
- FROM build_drafts
- WHERE id = ?`, id)
- return scanDraft(row.Scan)
- }
-
- func (s *Store) ListDrafts(ctx context.Context, limit int) ([]domain.BuildDraft, error) {
- query := `
- SELECT id, template_id, manifest_id, source, request_name, global_data_json, field_values_json, draft_context_json, status, notes, created_at, updated_at
- FROM build_drafts
- ORDER BY updated_at DESC`
- args := make([]any, 0, 1)
- if limit > 0 {
- query += " LIMIT ?"
- args = append(args, limit)
- }
- rows, err := s.db.QueryContext(ctx, query, args...)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- out := make([]domain.BuildDraft, 0)
- for rows.Next() {
- draft, err := scanDraft(rows.Scan)
- if err != nil {
- return nil, err
- }
- out = append(out, *draft)
- }
- return out, rows.Err()
- }
-
- func runMigrations(db *sql.DB) error {
- if _, err := db.Exec(`
- CREATE TABLE IF NOT EXISTS schema_migrations (
- version TEXT PRIMARY KEY,
- applied_at TEXT NOT NULL
- )`); err != nil {
- return err
- }
-
- entries, err := fs.ReadDir(migrationFS, "migrations")
- if err != nil {
- return err
- }
- files := make([]string, 0, len(entries))
- for _, e := range entries {
- if e.IsDir() || !strings.HasSuffix(e.Name(), ".sql") {
- continue
- }
- files = append(files, e.Name())
- }
- sort.Strings(files)
-
- for _, name := range files {
- var exists int
- if err := db.QueryRow(`SELECT COUNT(1) FROM schema_migrations WHERE version = ?`, name).Scan(&exists); err != nil {
- return err
- }
- if exists > 0 {
- continue
- }
-
- raw, err := migrationFS.ReadFile("migrations/" + name)
- if err != nil {
- return err
- }
- tx, err := db.Begin()
- if err != nil {
- return err
- }
- if _, err := tx.Exec(string(raw)); err != nil {
- _ = tx.Rollback()
- return fmt.Errorf("apply %s: %w", name, err)
- }
- if _, err := tx.Exec(`INSERT INTO schema_migrations(version, applied_at) VALUES(?, ?)`, name, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
- _ = tx.Rollback()
- return err
- }
- if err := tx.Commit(); err != nil {
- return err
- }
- }
- return nil
- }
-
- func scanTemplate(scan func(dest ...any) error) (*domain.Template, error) {
- var t domain.Template
- var paletteReady int
- var isAITemplate int
- var isOnboarded int
- var lastDiscovered sql.NullString
- var raw []byte
- if err := scan(
- &t.ID, &t.Name, &t.Description, &t.Locale, &t.ThumbnailURL, &t.TemplatePreviewURL, &t.Type,
- &paletteReady, &raw, &isAITemplate, &isOnboarded, &t.ManifestStatus, &lastDiscovered,
- ); err != nil {
- if err == sql.ErrNoRows {
- return nil, store.ErrNotFound
- }
- return nil, err
- }
- t.PaletteReady = paletteReady == 1
- t.IsAITemplate = isAITemplate == 1
- t.IsOnboarded = isOnboarded == 1
- t.RawJSON = cloneBytes(raw)
- if lastDiscovered.Valid {
- if ts, err := time.Parse(time.RFC3339Nano, lastDiscovered.String); err == nil {
- t.LastDiscoveredAt = &ts
- }
- }
- return &t, nil
- }
-
- func scanManifest(scan func(dest ...any) error) (*domain.TemplateManifest, error) {
- var m domain.TemplateManifest
- var isActive int
- var payloadRaw []byte
- var responseRaw []byte
- var flattenedRaw []byte
- var createdAtRaw string
- var updatedAtRaw string
- if err := scan(
- &m.ID, &m.TemplateID, &m.Version, &m.Source, &m.LanguageUsedDiscovery, &payloadRaw,
- &responseRaw, &flattenedRaw, &isActive, &createdAtRaw, &updatedAtRaw,
- ); err != nil {
- if err == sql.ErrNoRows {
- return nil, store.ErrNotFound
- }
- return nil, err
- }
- m.IsActive = isActive == 1
- m.DiscoveryPayloadJSON = cloneBytes(payloadRaw)
- m.DiscoveryResponseJSON = cloneBytes(responseRaw)
- m.FlattenedManifestJSON = cloneBytes(flattenedRaw)
- m.CreatedAt, _ = time.Parse(time.RFC3339Nano, createdAtRaw)
- m.UpdatedAt, _ = time.Parse(time.RFC3339Nano, updatedAtRaw)
- return &m, nil
- }
-
- func scanBuild(scan func(dest ...any) error) (*domain.SiteBuild, error) {
- var b domain.SiteBuild
- var globalRaw []byte
- var aiDataRaw []byte
- var payloadRaw []byte
- var resultRaw []byte
- var errorRaw []byte
- var startedAtRaw sql.NullString
- var finishedAtRaw sql.NullString
- var jobID sql.NullInt64
- var siteID sql.NullInt64
- if err := scan(
- &b.ID, &b.TemplateID, &b.ManifestID, &b.RequestName, &globalRaw, &aiDataRaw, &payloadRaw,
- &jobID, &siteID, &b.QCStatus, &b.QCPreviewURL, &b.QCEditorURL, &resultRaw, &errorRaw, &startedAtRaw, &finishedAtRaw,
- ); err != nil {
- if err == sql.ErrNoRows {
- return nil, store.ErrNotFound
- }
- return nil, err
- }
- b.GlobalDataJSON = cloneBytes(globalRaw)
- b.AIDataJSON = cloneBytes(aiDataRaw)
- b.FinalSitesPayload = cloneBytes(payloadRaw)
- b.QCResultJSON = cloneBytes(resultRaw)
- b.QCErrorJSON = cloneBytes(errorRaw)
- if jobID.Valid {
- id := jobID.Int64
- b.QCJobID = &id
- }
- if siteID.Valid {
- id := siteID.Int64
- b.QCSiteID = &id
- }
- b.StartedAt = parseTimePtr(startedAtRaw)
- b.FinishedAt = parseTimePtr(finishedAtRaw)
- return &b, nil
- }
-
- func scanDraft(scan func(dest ...any) error) (*domain.BuildDraft, error) {
- var d domain.BuildDraft
- var templateID sql.NullInt64
- var globalRaw []byte
- var fieldsRaw []byte
- var draftContextRaw []byte
- var createdAtRaw string
- var updatedAtRaw string
- if err := scan(
- &d.ID, &templateID, &d.ManifestID, &d.Source, &d.RequestName, &globalRaw, &fieldsRaw, &draftContextRaw, &d.Status, &d.Notes, &createdAtRaw, &updatedAtRaw,
- ); err != nil {
- if err == sql.ErrNoRows {
- return nil, store.ErrNotFound
- }
- return nil, err
- }
- if templateID.Valid {
- d.TemplateID = templateID.Int64
- }
- d.GlobalDataJSON = cloneBytes(globalRaw)
- d.FieldValuesJSON = cloneBytes(fieldsRaw)
- d.DraftContextJSON = cloneBytes(draftContextRaw)
- d.CreatedAt, _ = time.Parse(time.RFC3339Nano, createdAtRaw)
- d.UpdatedAt, _ = time.Parse(time.RFC3339Nano, updatedAtRaw)
- return &d, nil
- }
-
- func rollback(tx *sql.Tx) {
- _ = tx.Rollback()
- }
-
- func boolToInt(v bool) int {
- if v {
- return 1
- }
- return 0
- }
-
- func defaultString(value, fallback string) string {
- if strings.TrimSpace(value) == "" {
- return fallback
- }
- return strings.TrimSpace(value)
- }
-
- func asRaw(raw json.RawMessage) []byte {
- if len(raw) == 0 {
- return nil
- }
- out := make([]byte, len(raw))
- copy(out, raw)
- return out
- }
-
- func cloneBytes(raw []byte) []byte {
- if len(raw) == 0 {
- return nil
- }
- out := make([]byte, len(raw))
- copy(out, raw)
- return out
- }
-
- func asRFC3339Ptr(t *time.Time) *string {
- if t == nil {
- return nil
- }
- v := t.UTC().Format(time.RFC3339Nano)
- return &v
- }
-
- func parseTimePtr(value sql.NullString) *time.Time {
- if !value.Valid {
- return nil
- }
- ts, err := time.Parse(time.RFC3339Nano, value.String)
- if err != nil {
- return nil
- }
- return &ts
- }
-
- func nullableInt64(value int64) any {
- if value <= 0 {
- return nil
- }
- return value
- }
|