Files
silo/internal/db/projects.go
forbes-0023 b3c748ef10 refactor: move sourcing_link and standard_cost from item columns to revision properties
- Add migration 013 to copy sourcing_link/standard_cost values into
  current revision properties JSONB and drop the columns from items table
- Remove SourcingLink/StandardCost from Go Item struct and all DB queries
  (items.go, audit_queries.go, projects.go)
- Remove from API request/response structs and handlers
- Update CSV/ODS/BOM export/import to read these from revision properties
- Update audit handlers to score as regular property fields
- Remove from frontend Item type and hardcoded form fields
- MainTab now reads sourcing_link/standard_cost from item.properties
- CreateItemPane/EditItemPane no longer have dedicated fields for these;
  they will be rendered as schema-driven property fields
2026-02-11 09:50:31 -06:00

305 lines
7.7 KiB
Go

package db
import (
"context"
"time"
"github.com/jackc/pgx/v5"
)
// Project represents a project in the database.
type Project struct {
ID string
Code string
Name string
Description string
CreatedAt time.Time
CreatedBy *string
}
// ProjectRepository provides project database operations.
type ProjectRepository struct {
db *DB
}
// NewProjectRepository creates a new project repository.
func NewProjectRepository(db *DB) *ProjectRepository {
return &ProjectRepository{db: db}
}
// List returns all projects.
func (r *ProjectRepository) List(ctx context.Context) ([]*Project, error) {
rows, err := r.db.pool.Query(ctx, `
SELECT id, code, name, description, created_at
FROM projects
ORDER BY code
`)
if err != nil {
return nil, err
}
defer rows.Close()
var projects []*Project
for rows.Next() {
p := &Project{}
var name, desc *string
if err := rows.Scan(&p.ID, &p.Code, &name, &desc, &p.CreatedAt); err != nil {
return nil, err
}
if name != nil {
p.Name = *name
}
if desc != nil {
p.Description = *desc
}
projects = append(projects, p)
}
return projects, rows.Err()
}
// GetByCode returns a project by its code.
func (r *ProjectRepository) GetByCode(ctx context.Context, code string) (*Project, error) {
p := &Project{}
var name, desc *string
err := r.db.pool.QueryRow(ctx, `
SELECT id, code, name, description, created_at
FROM projects
WHERE code = $1
`, code).Scan(&p.ID, &p.Code, &name, &desc, &p.CreatedAt)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
if name != nil {
p.Name = *name
}
if desc != nil {
p.Description = *desc
}
return p, nil
}
// GetByID returns a project by its ID.
func (r *ProjectRepository) GetByID(ctx context.Context, id string) (*Project, error) {
p := &Project{}
var name, desc *string
err := r.db.pool.QueryRow(ctx, `
SELECT id, code, name, description, created_at
FROM projects
WHERE id = $1
`, id).Scan(&p.ID, &p.Code, &name, &desc, &p.CreatedAt)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
if name != nil {
p.Name = *name
}
if desc != nil {
p.Description = *desc
}
return p, nil
}
// Create inserts a new project.
func (r *ProjectRepository) Create(ctx context.Context, p *Project) error {
return r.db.pool.QueryRow(ctx, `
INSERT INTO projects (code, name, description, created_by)
VALUES ($1, $2, $3, $4)
RETURNING id, created_at
`, p.Code, nullIfEmpty(p.Name), nullIfEmpty(p.Description), p.CreatedBy).Scan(&p.ID, &p.CreatedAt)
}
// Update updates a project's name and description.
func (r *ProjectRepository) Update(ctx context.Context, code string, name, description string) error {
_, err := r.db.pool.Exec(ctx, `
UPDATE projects
SET name = $2, description = $3
WHERE code = $1
`, code, nullIfEmpty(name), nullIfEmpty(description))
return err
}
// Delete removes a project (will cascade to item_projects).
func (r *ProjectRepository) Delete(ctx context.Context, code string) error {
_, err := r.db.pool.Exec(ctx, `DELETE FROM projects WHERE code = $1`, code)
return err
}
// AddItemToProject associates an item with a project.
func (r *ProjectRepository) AddItemToProject(ctx context.Context, itemID, projectID string) error {
_, err := r.db.pool.Exec(ctx, `
INSERT INTO item_projects (item_id, project_id)
VALUES ($1, $2)
ON CONFLICT (item_id, project_id) DO NOTHING
`, itemID, projectID)
return err
}
// AddItemToProjectByCode associates an item with a project by code.
func (r *ProjectRepository) AddItemToProjectByCode(ctx context.Context, itemID, projectCode string) error {
_, err := r.db.pool.Exec(ctx, `
INSERT INTO item_projects (item_id, project_id)
SELECT $1, id FROM projects WHERE code = $2
ON CONFLICT (item_id, project_id) DO NOTHING
`, itemID, projectCode)
return err
}
// RemoveItemFromProject removes an item's association with a project.
func (r *ProjectRepository) RemoveItemFromProject(ctx context.Context, itemID, projectID string) error {
_, err := r.db.pool.Exec(ctx, `
DELETE FROM item_projects
WHERE item_id = $1 AND project_id = $2
`, itemID, projectID)
return err
}
// RemoveItemFromProjectByCode removes an item's association with a project by code.
func (r *ProjectRepository) RemoveItemFromProjectByCode(ctx context.Context, itemID, projectCode string) error {
_, err := r.db.pool.Exec(ctx, `
DELETE FROM item_projects
WHERE item_id = $1 AND project_id = (SELECT id FROM projects WHERE code = $2)
`, itemID, projectCode)
return err
}
// GetProjectsForItem returns all projects associated with an item.
func (r *ProjectRepository) GetProjectsForItem(ctx context.Context, itemID string) ([]*Project, error) {
rows, err := r.db.pool.Query(ctx, `
SELECT p.id, p.code, p.name, p.description, p.created_at
FROM projects p
JOIN item_projects ip ON ip.project_id = p.id
WHERE ip.item_id = $1
ORDER BY p.code
`, itemID)
if err != nil {
return nil, err
}
defer rows.Close()
var projects []*Project
for rows.Next() {
p := &Project{}
var name, desc *string
if err := rows.Scan(&p.ID, &p.Code, &name, &desc, &p.CreatedAt); err != nil {
return nil, err
}
if name != nil {
p.Name = *name
}
if desc != nil {
p.Description = *desc
}
projects = append(projects, p)
}
return projects, rows.Err()
}
// GetProjectCodesForItem returns project codes for an item (convenience method).
func (r *ProjectRepository) GetProjectCodesForItem(ctx context.Context, itemID string) ([]string, error) {
rows, err := r.db.pool.Query(ctx, `
SELECT p.code
FROM projects p
JOIN item_projects ip ON ip.project_id = p.id
WHERE ip.item_id = $1
ORDER BY p.code
`, itemID)
if err != nil {
return nil, err
}
defer rows.Close()
var codes []string
for rows.Next() {
var code string
if err := rows.Scan(&code); err != nil {
return nil, err
}
codes = append(codes, code)
}
return codes, rows.Err()
}
// GetItemsForProject returns all items associated with a project.
func (r *ProjectRepository) GetItemsForProject(ctx context.Context, projectID string) ([]*Item, error) {
rows, err := r.db.pool.Query(ctx, `
SELECT i.id, i.part_number, i.schema_id, i.item_type, i.description,
i.created_at, i.updated_at, i.archived_at, i.current_revision,
i.cad_synced_at, i.cad_file_path,
i.sourcing_type, i.long_description,
i.thumbnail_key
FROM items i
JOIN item_projects ip ON ip.item_id = i.id
WHERE ip.project_id = $1 AND i.archived_at IS NULL
ORDER BY i.part_number
`, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []*Item
for rows.Next() {
item := &Item{}
if err := rows.Scan(
&item.ID, &item.PartNumber, &item.SchemaID, &item.ItemType, &item.Description,
&item.CreatedAt, &item.UpdatedAt, &item.ArchivedAt, &item.CurrentRevision,
&item.CADSyncedAt, &item.CADFilePath,
&item.SourcingType, &item.LongDescription,
&item.ThumbnailKey,
); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
// SetItemProjects replaces all project associations for an item.
func (r *ProjectRepository) SetItemProjects(ctx context.Context, itemID string, projectCodes []string) error {
return r.db.Tx(ctx, func(tx pgx.Tx) error {
// Remove existing associations
_, err := tx.Exec(ctx, `DELETE FROM item_projects WHERE item_id = $1`, itemID)
if err != nil {
return err
}
// Add new associations
for _, code := range projectCodes {
_, err := tx.Exec(ctx, `
INSERT INTO item_projects (item_id, project_id)
SELECT $1, id FROM projects WHERE code = $2
ON CONFLICT (item_id, project_id) DO NOTHING
`, itemID, code)
if err != nil {
return err
}
}
return nil
})
}
// helper function
func nullIfEmpty(s string) *string {
if s == "" {
return nil
}
return &s
}