chore(seed): t-paliad-256 — wipe + seed Example Projects exercising chain code
Re-runnable Go script under scripts/seed-example-projects/ that wipes every paliad.projects row (FK CASCADE handles dependents) and seeds 18 realistic patent-litigation projects across 3 example clients: SIEMENS — UPC + LG cases incl. CCR (Widerklage) on EP3456789 BAYER — EPA Einspruch + BPatG Nichtigkeit on EP2222333 BEISPL — sparse DPMA demo on DE10987654 Every node carries the chain-code-driving fields (reference on client, opponent_code on litigation, patent_number on patent, proceeding_type_id on case), producing codes like SIEMENS.HUAW.789.INF.CFI and SIEMENS.HUAW.789.CCR.CFI via services.BuildProjectCode. One transaction wraps both wipe and seed; -dry-run rolls back so the script can be sanity-checked before commit. Reference tables (proceeding_types, deadline_rules, event_types, gerichte, checklists templates, firms) are untouched. Ran live against youpc Postgres 2026-05-25: 12 rows wiped, 18 seeded.
This commit is contained in:
568
scripts/seed-example-projects/main.go
Normal file
568
scripts/seed-example-projects/main.go
Normal file
@@ -0,0 +1,568 @@
|
||||
// Seed Example Projects (t-paliad-256 / m/paliad#87).
|
||||
//
|
||||
// Re-runnable test-data reset:
|
||||
//
|
||||
// 1. Wipes every row in paliad.projects (FK CASCADE handles the
|
||||
// dependent rows: deadlines, appointments, parties, notes,
|
||||
// project_events, project_teams, submission_drafts, approval_*,
|
||||
// project_partner_units, user_pinned_projects, documents,
|
||||
// user_calendar_bindings).
|
||||
//
|
||||
// 2. Inserts a small but realistic example tree (3 clients, 4
|
||||
// litigations, 4 patents, 8 cases — 19 projects total) that
|
||||
// exercises the auto-derived chain code: Client.Litigation.Patent.Case
|
||||
// → e.g. SIEMENS.HUAW.789.INF.CFI.
|
||||
//
|
||||
// 3. Re-reads the projects and prints each row's chain code so the
|
||||
// operator can eyeball the result without bouncing to SQL.
|
||||
//
|
||||
// Reference tables (proceeding_types, deadline_rules, event_types,
|
||||
// gerichte, checklists templates, firms, profiles) are untouched.
|
||||
//
|
||||
// Run:
|
||||
//
|
||||
// DATABASE_URL='postgres://...' go run ./scripts/seed-example-projects
|
||||
//
|
||||
// One transaction wraps both wipe and seed so the DB is never in a
|
||||
// half-wiped state. Re-running drops the previous example tree and
|
||||
// reseeds fresh UUIDs — handy when project-code semantics change.
|
||||
//
|
||||
// Owner: m (matthias.siebels@hoganlovells.com). The script looks the
|
||||
// auth user up by email so it works on any environment where that
|
||||
// account exists; on a brand-new DB it falls back to NULL created_by.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"mgit.msbls.de/m/paliad/internal/services"
|
||||
)
|
||||
|
||||
// ownerEmail is the auth.users email the seed assigns as created_by.
|
||||
// Living in code (not a flag) because the example tree is m-owned by
|
||||
// convention; flip if the example data ever needs a service-account
|
||||
// owner.
|
||||
const ownerEmail = "matthias.siebels@hoganlovells.com"
|
||||
|
||||
// Proceeding-type IDs used by the seed. Resolved by code (not pinned
|
||||
// to integer IDs in source) to survive DB renumbering. Loaded once at
|
||||
// startup; missing codes fail fast with a clear message.
|
||||
var proceedingCodes = []string{
|
||||
"upc.inf.cfi",
|
||||
"upc.ccr.cfi",
|
||||
"upc.apl.merits",
|
||||
"de.inf.lg",
|
||||
"epa.opp.opd",
|
||||
"de.null.bpatg",
|
||||
"dpma.opp.dpma",
|
||||
}
|
||||
|
||||
func main() {
|
||||
dsn := flag.String("dsn", os.Getenv("DATABASE_URL"), "Postgres DSN (defaults to $DATABASE_URL)")
|
||||
dryRun := flag.Bool("dry-run", false, "print intended actions, roll back transaction")
|
||||
flag.Parse()
|
||||
|
||||
if *dsn == "" {
|
||||
fmt.Fprintln(os.Stderr, "seed-example-projects: DATABASE_URL not set and -dsn empty")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db, err := sqlx.Connect("postgres", *dsn)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "connect:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
if err := run(ctx, db, *dryRun); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "seed-example-projects:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context, db *sqlx.DB, dryRun bool) error {
|
||||
ownerID, err := lookupOwner(ctx, db, ownerEmail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("lookup owner: %w", err)
|
||||
}
|
||||
if ownerID == uuid.Nil {
|
||||
fmt.Printf("note: %s not found in auth.users — created_by will be NULL\n", ownerEmail)
|
||||
} else {
|
||||
fmt.Printf("owner resolved: %s = %s\n", ownerEmail, ownerID)
|
||||
}
|
||||
|
||||
procIDs, err := lookupProceedingTypes(ctx, db, proceedingCodes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("lookup proceeding_types: %w", err)
|
||||
}
|
||||
|
||||
tx, err := db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }() // no-op if Commit ran first
|
||||
|
||||
if err := wipe(ctx, tx); err != nil {
|
||||
return fmt.Errorf("wipe: %w", err)
|
||||
}
|
||||
|
||||
tree, err := seed(ctx, tx, ownerID, procIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("seed: %w", err)
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
fmt.Println("\n--- DRY RUN — rolling back ---")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
fmt.Println("seed committed.")
|
||||
|
||||
if err := report(ctx, db, tree); err != nil {
|
||||
return fmt.Errorf("report: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupOwner(ctx context.Context, db *sqlx.DB, email string) (uuid.UUID, error) {
|
||||
var id uuid.UUID
|
||||
err := db.GetContext(ctx, &id, `SELECT id FROM auth.users WHERE email = $1`, email)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return uuid.Nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func lookupProceedingTypes(ctx context.Context, db *sqlx.DB, codes []string) (map[string]int, error) {
|
||||
rows, err := db.QueryxContext(ctx,
|
||||
`SELECT id, code FROM paliad.proceeding_types WHERE code = ANY($1)`,
|
||||
pgTextArray(codes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make(map[string]int, len(codes))
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var code string
|
||||
if err := rows.Scan(&id, &code); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[code] = id
|
||||
}
|
||||
for _, c := range codes {
|
||||
if _, ok := out[c]; !ok {
|
||||
return nil, fmt.Errorf("proceeding_types row missing for code=%q", c)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// pgTextArray is the lib/pq array adapter, repackaged inline so the
|
||||
// script doesn't need a separate util import.
|
||||
func pgTextArray(xs []string) any {
|
||||
type arr = []string
|
||||
return arr(xs)
|
||||
}
|
||||
|
||||
// wipe deletes every paliad.projects row. FK CASCADE handles the
|
||||
// dependent tables (verified live 2026-05-25 against information_schema:
|
||||
// appointments, approval_requests, approval_policies, deadlines,
|
||||
// documents, notes, parties, project_events, project_partner_units,
|
||||
// project_teams, submission_drafts, user_pinned_projects,
|
||||
// user_calendar_bindings, checklist_shares all cascade; projects.
|
||||
// counterclaim_of and checklist_instances SET NULL; policy_audit_log
|
||||
// SET NULL).
|
||||
//
|
||||
// Reference tables (proceeding_types, deadline_rules, event_types,
|
||||
// gerichte, checklists, firms, partner_units, profiles) are not
|
||||
// referenced from this delete.
|
||||
func wipe(ctx context.Context, tx *sqlx.Tx) error {
|
||||
res, err := tx.ExecContext(ctx, `DELETE FROM paliad.projects`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
fmt.Printf("wiped: %d project rows (FK CASCADE handled dependents)\n", n)
|
||||
return nil
|
||||
}
|
||||
|
||||
// seededNode is one row of the seed result, kept so we can print the
|
||||
// chain code after commit without re-querying for IDs.
|
||||
type seededNode struct {
|
||||
id uuid.UUID
|
||||
title string
|
||||
}
|
||||
|
||||
// seed inserts the example tree. Order matters because parent_id FKs
|
||||
// must already exist — clients first, then litigations under them, then
|
||||
// patents, then cases (with the CCR case referencing its sibling
|
||||
// Klage case via counterclaim_of).
|
||||
func seed(ctx context.Context, tx *sqlx.Tx, ownerID uuid.UUID, procIDs map[string]int) ([]seededNode, error) {
|
||||
var nodes []seededNode
|
||||
|
||||
insertProject := func(p projectInsert) (uuid.UUID, error) {
|
||||
id := uuid.New()
|
||||
var createdBy any
|
||||
if ownerID != uuid.Nil {
|
||||
createdBy = ownerID
|
||||
}
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO paliad.projects (
|
||||
id, type, parent_id, title, reference, description, status,
|
||||
created_by, industry, country, client_number, matter_number,
|
||||
patent_number, filing_date, grant_date,
|
||||
court, case_number, proceeding_type_id,
|
||||
our_side, opponent_code, instance_level, counterclaim_of
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, 'active',
|
||||
$7, $8, $9, $10, $11,
|
||||
$12, $13, $14,
|
||||
$15, $16, $17,
|
||||
$18, $19, $20, $21
|
||||
)`,
|
||||
id, p.Type, nullUUID(p.ParentID), p.Title, nullStr(p.Reference), nullStr(p.Description),
|
||||
createdBy, nullStr(p.Industry), nullStr(p.Country), nullStr(p.ClientNumber), nullStr(p.MatterNumber),
|
||||
nullStr(p.PatentNumber), nullDate(p.FilingDate), nullDate(p.GrantDate),
|
||||
nullStr(p.Court), nullStr(p.CaseNumber), nullInt(p.ProceedingTypeID),
|
||||
nullStr(p.OurSide), nullStr(p.OpponentCode), nullStr(p.InstanceLevel), nullUUID(p.CounterclaimOf),
|
||||
)
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("insert %s %q: %w", p.Type, p.Title, err)
|
||||
}
|
||||
nodes = append(nodes, seededNode{id: id, title: p.Title})
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// --- Client 1: Siemens AG ----------------------------------------
|
||||
siemens, err := insertProject(projectInsert{
|
||||
Type: "client", Title: "Siemens AG", Reference: "SIEMENS",
|
||||
Industry: "Telekommunikation / Industrieelektronik", Country: "DE",
|
||||
Description: "Beispiel-Mandant — Telekommunikation & Halbleiter.",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
siemensHuawei, err := insertProject(projectInsert{
|
||||
Type: "litigation", ParentID: siemens,
|
||||
Title: "Siemens ./. Huawei Technologies", OpponentCode: "HUAW",
|
||||
Description: "Patentstreit Mobilfunk-Standardpatent.", OurSide: "claimant",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
siemensHuaweiPatent, err := insertProject(projectInsert{
|
||||
Type: "patent", ParentID: siemensHuawei,
|
||||
Title: "EP3456789 — Funkkommunikationssystem mit Mehrfachantenne",
|
||||
PatentNumber: "EP3456789",
|
||||
FilingDate: "2018-03-12", GrantDate: "2022-11-09",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
upcInfCFI, err := insertProject(projectInsert{
|
||||
Type: "case", ParentID: siemensHuaweiPatent,
|
||||
Title: "UPC CFI München — Klage Siemens ./. Huawei (EP3456789)",
|
||||
Court: "UPC Lokalkammer München",
|
||||
CaseNumber: "UPC_CFI_123/2026",
|
||||
ProceedingTypeID: procIDs["upc.inf.cfi"],
|
||||
OurSide: "claimant",
|
||||
InstanceLevel: "first",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = insertProject(projectInsert{
|
||||
Type: "case", ParentID: siemensHuaweiPatent,
|
||||
Title: "UPC CFI München — Widerklage Huawei ./. Siemens (EP3456789)",
|
||||
Court: "UPC Lokalkammer München",
|
||||
CaseNumber: "UPC_CFI_123/2026 (CCR)",
|
||||
ProceedingTypeID: procIDs["upc.ccr.cfi"],
|
||||
OurSide: "defendant", // we're respondent on the CCR
|
||||
InstanceLevel: "first",
|
||||
CounterclaimOf: upcInfCFI,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = insertProject(projectInsert{
|
||||
Type: "case", ParentID: siemensHuaweiPatent,
|
||||
Title: "UPC Berufungsgericht — Berufung Huawei (EP3456789)",
|
||||
Court: "UPC Court of Appeal",
|
||||
CaseNumber: "UPC_CoA_45/2027",
|
||||
ProceedingTypeID: procIDs["upc.apl.merits"],
|
||||
OurSide: "respondent",
|
||||
InstanceLevel: "appeal",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
siemensBosch, err := insertProject(projectInsert{
|
||||
Type: "litigation", ParentID: siemens,
|
||||
Title: "Siemens ./. Robert Bosch GmbH", OpponentCode: "BOSCH",
|
||||
Description: "Sensorik / autonomes Fahren.", OurSide: "claimant",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
siemensBoschPatent, err := insertProject(projectInsert{
|
||||
Type: "patent", ParentID: siemensBosch,
|
||||
Title: "EP1111222 — Sensoreinrichtung für autonomes Fahren",
|
||||
PatentNumber: "EP1111222",
|
||||
FilingDate: "2017-06-21", GrantDate: "2021-08-04",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = insertProject(projectInsert{
|
||||
Type: "case", ParentID: siemensBoschPatent,
|
||||
Title: "LG München I — Klage Siemens ./. Bosch (EP1111222)",
|
||||
Court: "Landgericht München I",
|
||||
CaseNumber: "7 O 12345/26",
|
||||
ProceedingTypeID: procIDs["de.inf.lg"],
|
||||
OurSide: "claimant",
|
||||
InstanceLevel: "first",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// --- Client 2: Bayer AG ------------------------------------------
|
||||
bayer, err := insertProject(projectInsert{
|
||||
Type: "client", Title: "Bayer AG", Reference: "BAYER",
|
||||
Industry: "Pharma / Life Sciences", Country: "DE",
|
||||
Description: "Beispiel-Mandant — pharmazeutische Wirkstoffe.",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bayerNova, err := insertProject(projectInsert{
|
||||
Type: "litigation", ParentID: bayer,
|
||||
Title: "Bayer ./. Novartis Pharma", OpponentCode: "NOVA",
|
||||
Description: "Wirkstoffverbindung X — Einspruch + Nichtigkeit.", OurSide: "claimant",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bayerNovaPatent, err := insertProject(projectInsert{
|
||||
Type: "patent", ParentID: bayerNova,
|
||||
Title: "EP2222333 — Wirkstoffverbindung X",
|
||||
PatentNumber: "EP2222333",
|
||||
FilingDate: "2015-09-30", GrantDate: "2020-04-22",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = insertProject(projectInsert{
|
||||
Type: "case", ParentID: bayerNovaPatent,
|
||||
Title: "EPA Einspruch — Novartis ./. EP2222333",
|
||||
Court: "Europäisches Patentamt — Einspruchsabteilung",
|
||||
CaseNumber: "OPP-2026-0042",
|
||||
ProceedingTypeID: procIDs["epa.opp.opd"],
|
||||
OurSide: "respondent", // Bayer is patent owner defending the patent
|
||||
InstanceLevel: "first",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = insertProject(projectInsert{
|
||||
Type: "case", ParentID: bayerNovaPatent,
|
||||
Title: "BPatG — Nichtigkeitsklage Novartis ./. EP2222333",
|
||||
Court: "Bundespatentgericht",
|
||||
CaseNumber: "5 Ni 12/26",
|
||||
ProceedingTypeID: procIDs["de.null.bpatg"],
|
||||
OurSide: "respondent",
|
||||
InstanceLevel: "first",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// --- Client 3: Beispiel AG (intentionally sparse) ----------------
|
||||
// Demonstrates the empty-segment skip in BuildProjectCode — the
|
||||
// case row has a proceeding_type set so the tail is present, but
|
||||
// no instance_level / our_side, and the patent's number is national
|
||||
// (DE) so the last-3-digits segment shows DE-style behaviour.
|
||||
beispiel, err := insertProject(projectInsert{
|
||||
Type: "client", Title: "Beispiel AG", Reference: "BEISPL",
|
||||
Industry: "Unspezifiziert", Country: "DE",
|
||||
Description: "Sparse-Beispiel — zeigt, wie fehlende Segmente übersprungen werden.",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
beispielWtb, err := insertProject(projectInsert{
|
||||
Type: "litigation", ParentID: beispiel,
|
||||
Title: "Beispiel ./. Wettbewerber GmbH", OpponentCode: "WTB",
|
||||
Description: "Demo-Litigation ohne große Detailtiefe.",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
beispielWtbPatent, err := insertProject(projectInsert{
|
||||
Type: "patent", ParentID: beispielWtb,
|
||||
Title: "DE10987654 — Demo-Erfindung",
|
||||
PatentNumber: "DE10987654",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = insertProject(projectInsert{
|
||||
Type: "case", ParentID: beispielWtbPatent,
|
||||
Title: "DPMA Einspruch — Wettbewerber ./. DE10987654",
|
||||
Court: "Deutsches Patent- und Markenamt",
|
||||
CaseNumber: "DPMA-EIN-987/26",
|
||||
ProceedingTypeID: procIDs["dpma.opp.dpma"],
|
||||
OurSide: "respondent",
|
||||
InstanceLevel: "first",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("seeded: %d projects\n", len(nodes))
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
// projectInsert is the typed input for one insertProject call. Pointer
|
||||
// fields are kept as plain strings here and converted via nullStr at
|
||||
// bind time; keeps the call sites readable.
|
||||
type projectInsert struct {
|
||||
Type string
|
||||
ParentID uuid.UUID
|
||||
Title string
|
||||
Reference string
|
||||
Description string
|
||||
Industry string
|
||||
Country string
|
||||
ClientNumber string
|
||||
MatterNumber string
|
||||
PatentNumber string
|
||||
FilingDate string // YYYY-MM-DD
|
||||
GrantDate string
|
||||
Court string
|
||||
CaseNumber string
|
||||
ProceedingTypeID int
|
||||
OurSide string
|
||||
OpponentCode string
|
||||
InstanceLevel string
|
||||
CounterclaimOf uuid.UUID
|
||||
}
|
||||
|
||||
func nullStr(s string) any {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func nullInt(i int) any {
|
||||
if i == 0 {
|
||||
return nil
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func nullUUID(u uuid.UUID) any {
|
||||
if u == uuid.Nil {
|
||||
return nil
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func nullDate(s string) any {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
t, err := time.Parse("2006-01-02", s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// reportRow is one row of the post-seed report — only the fields the
|
||||
// printout needs.
|
||||
type reportRow struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
Type string `db:"type"`
|
||||
Title string `db:"title"`
|
||||
Path string `db:"path"`
|
||||
}
|
||||
|
||||
// report prints the seeded tree with the auto-derived chain code for
|
||||
// each row. Uses services.BuildProjectCode so the script verifies the
|
||||
// same helper the live app uses (catches drift if the algorithm
|
||||
// changes).
|
||||
func report(ctx context.Context, db *sqlx.DB, _ []seededNode) error {
|
||||
var rows []reportRow
|
||||
err := db.SelectContext(ctx, &rows, `
|
||||
SELECT id, type, title, path
|
||||
FROM paliad.projects
|
||||
ORDER BY path
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\nresulting chain codes:")
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "TYPE\tTITLE\tCODE")
|
||||
for _, r := range rows {
|
||||
code, err := services.BuildProjectCode(ctx, db, r.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build code for %s: %w", r.ID, err)
|
||||
}
|
||||
indent := strings.Repeat(" ", pathDepth(r.Path)-1)
|
||||
fmt.Fprintf(tw, "%s\t%s%s\t%s\n", r.Type, indent, r.Title, code)
|
||||
}
|
||||
return tw.Flush()
|
||||
}
|
||||
|
||||
func pathDepth(p string) int {
|
||||
if p == "" {
|
||||
return 1
|
||||
}
|
||||
d := 1
|
||||
for _, c := range p {
|
||||
if c == '.' {
|
||||
d++
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
Reference in New Issue
Block a user