- Implement standalone Go backend API server using Gin and pure-Go SQLite (modernc.org/sqlite) - Add 11-table schema DDL migrations (000001_init.up.sql) based on DATA_TABLE.md - Add seed data ingestion tool (cmd/seed) and static TypeScript content exporter (cmd/export-content) - Add configuration loader (internal/config) supporting environment variables and .env files (HOST, PORT, DB_PATH, GIN_MODE, CORS_ALLOW_ORIGINS, AUTO_MIGRATE) - Add lightweight multi-stage Dockerfile and docker-compose.yml with persistent SQLite volume - Add comprehensive architecture specification (ARCHITECTURE.md) and REST API interface specification (API_INTERFACE.md) - Harmonize frontend publications page to consume isHighlight flag from database-synced content
345 lines
11 KiB
Go
345 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"git.godopu.com/lab/landing_page/backend/internal/config"
|
|
"git.godopu.com/lab/landing_page/backend/internal/db"
|
|
"git.godopu.com/lab/landing_page/backend/internal/repository"
|
|
)
|
|
|
|
func marshalJSONNoEscapeHTML(data any) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
enc := json.NewEncoder(&buf)
|
|
enc.SetEscapeHTML(false)
|
|
enc.SetIndent("", " ")
|
|
if err := enc.Encode(data); err != nil {
|
|
return nil, err
|
|
}
|
|
return bytes.TrimRight(buf.Bytes(), "\n"), nil
|
|
}
|
|
|
|
func formatTSFile(importStmt, exportConstName, typeAnnotation string, data any) ([]byte, error) {
|
|
jsonData, err := marshalJSONNoEscapeHTML(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal JSON: %w", err)
|
|
}
|
|
|
|
header := "// AUTO-GENERATED by backend/cmd/export-content — DO NOT EDIT BY HAND\n"
|
|
content := fmt.Sprintf("%s%s\n\nexport const %s: %s = %s;\n", header, importStmt, exportConstName, typeAnnotation, string(jsonData))
|
|
return []byte(content), nil
|
|
}
|
|
|
|
func writeFileSafe(path string, data []byte) error {
|
|
return os.WriteFile(path, data, 0644)
|
|
}
|
|
|
|
func main() {
|
|
config.LoadEnvFile()
|
|
defaultDB := "anl.db"
|
|
if envDB := os.Getenv("DB_PATH"); envDB != "" {
|
|
defaultDB = envDB
|
|
}
|
|
|
|
dbPath := flag.String("db", defaultDB, "Path to SQLite database file")
|
|
targetDir := flag.String("out-dir", "../refer_landing_page/content", "Target content directory")
|
|
flag.Parse()
|
|
|
|
log.Printf("Exporting content from SQLite (%s) to TypeScript files in %s...", *dbPath, *targetDir)
|
|
database, err := db.Connect(*dbPath)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to database: %v", err)
|
|
}
|
|
defer database.Close()
|
|
|
|
ctx := context.Background()
|
|
homeRepo := repository.NewHomeRepository(database)
|
|
membersRepo := repository.NewMembersRepository(database)
|
|
pubsRepo := repository.NewPublicationsRepository(database)
|
|
lecturesRepo := repository.NewLecturesRepository(database)
|
|
standardsRepo := repository.NewStandardizationRepository(database)
|
|
|
|
// 1. Projects
|
|
projects, err := homeRepo.GetResearchProjects(ctx)
|
|
if err != nil {
|
|
log.Fatalf("Get research projects: %v", err)
|
|
}
|
|
type TSProject struct {
|
|
Slug string `json:"slug"`
|
|
Title string `json:"title"`
|
|
Abstract string `json:"abstract"`
|
|
Keywords []string `json:"keywords"`
|
|
StandardsTrack *string `json:"standardsTrack,omitempty"`
|
|
StandardsOrg *string `json:"standardsOrg,omitempty"`
|
|
Period *string `json:"period,omitempty"`
|
|
Deliverables []any `json:"deliverables"`
|
|
Funder *string `json:"funder,omitempty"`
|
|
}
|
|
var tsProjects []TSProject
|
|
for _, p := range projects {
|
|
tsProjects = append(tsProjects, TSProject{
|
|
Slug: p.Slug,
|
|
Title: p.Title,
|
|
Abstract: p.Abstract,
|
|
Keywords: p.Keywords,
|
|
StandardsTrack: p.Organization,
|
|
StandardsOrg: p.StandardsOrg,
|
|
Period: p.Period,
|
|
Deliverables: []any{},
|
|
Funder: p.Funder,
|
|
})
|
|
}
|
|
pBytes, err := formatTSFile(`import { ResearchProject } from "./types";`, "researchProjects", "ResearchProject[]", tsProjects)
|
|
if err != nil {
|
|
log.Fatalf("Format projects.ts failed: %v", err)
|
|
}
|
|
if err := writeFileSafe(filepath.Join(*targetDir, "projects.ts"), pBytes); err != nil {
|
|
log.Fatalf("Write projects.ts failed: %v", err)
|
|
}
|
|
log.Println("✓ Exported projects.ts")
|
|
|
|
// 2. Members & Alumni
|
|
members, err := membersRepo.GetMembers(ctx)
|
|
if err != nil {
|
|
log.Fatalf("Get members: %v", err)
|
|
}
|
|
type TSMember struct {
|
|
Ko string `json:"ko"`
|
|
En string `json:"en"`
|
|
Degree string `json:"degree"`
|
|
Affiliation *string `json:"affiliation,omitempty"`
|
|
}
|
|
var tsMembers []TSMember
|
|
for _, m := range members {
|
|
tsMembers = append(tsMembers, TSMember{
|
|
Ko: m.NameKo,
|
|
En: m.NameEn,
|
|
Degree: m.Degree,
|
|
Affiliation: m.Affiliation,
|
|
})
|
|
}
|
|
|
|
alumni, err := membersRepo.GetAlumni(ctx)
|
|
if err != nil {
|
|
log.Fatalf("Get alumni: %v", err)
|
|
}
|
|
type TSAlumnus struct {
|
|
Ko string `json:"ko"`
|
|
En string `json:"en"`
|
|
Degree string `json:"degree"`
|
|
GraduatedAt string `json:"graduatedAt"`
|
|
Major *string `json:"major,omitempty"`
|
|
CurrentPosition *string `json:"currentPosition,omitempty"`
|
|
}
|
|
var tsAlumni []TSAlumnus
|
|
for _, a := range alumni {
|
|
tsAlumni = append(tsAlumni, TSAlumnus{
|
|
Ko: a.NameKo,
|
|
En: a.NameEn,
|
|
Degree: a.Degree,
|
|
GraduatedAt: a.GraduatedAt,
|
|
Major: a.Major,
|
|
CurrentPosition: a.CurrentPosition,
|
|
})
|
|
}
|
|
|
|
mJSON, err := marshalJSONNoEscapeHTML(tsMembers)
|
|
if err != nil {
|
|
log.Fatalf("Marshal members failed: %v", err)
|
|
}
|
|
aJSON, err := marshalJSONNoEscapeHTML(tsAlumni)
|
|
if err != nil {
|
|
log.Fatalf("Marshal alumni failed: %v", err)
|
|
}
|
|
membersTS := fmt.Sprintf("// AUTO-GENERATED by backend/cmd/export-content — DO NOT EDIT BY HAND\nimport { Member, Alumnus } from \"./types\";\n\nexport const activeMembers: Member[] = %s;\n\nexport const alumni: Alumnus[] = %s;\n", string(mJSON), string(aJSON))
|
|
if err := writeFileSafe(filepath.Join(*targetDir, "members.ts"), []byte(membersTS)); err != nil {
|
|
log.Fatalf("Write members.ts failed: %v", err)
|
|
}
|
|
log.Println("✓ Exported members.ts")
|
|
|
|
// 3. Publications & Patents
|
|
pubs, err := pubsRepo.GetPublications(ctx, "")
|
|
if err != nil {
|
|
log.Fatalf("Get publications: %v", err)
|
|
}
|
|
type TSPublication struct {
|
|
Category string `json:"category"`
|
|
Title string `json:"title"`
|
|
Authors *string `json:"authors,omitempty"`
|
|
Venue string `json:"venue"`
|
|
Volume *string `json:"volume,omitempty"`
|
|
PublishedAt string `json:"publishedAt"`
|
|
KCI bool `json:"kci,omitempty"`
|
|
DOI *string `json:"doi,omitempty"`
|
|
IsHighlight bool `json:"isHighlight,omitempty"`
|
|
}
|
|
var tsPubs []TSPublication
|
|
for _, p := range pubs {
|
|
tsPubs = append(tsPubs, TSPublication{
|
|
Category: p.Category,
|
|
Title: p.Title,
|
|
Authors: p.Authors,
|
|
Venue: p.Venue,
|
|
Volume: p.Volume,
|
|
PublishedAt: p.PublishedAt,
|
|
KCI: p.KCI,
|
|
DOI: p.DOI,
|
|
IsHighlight: p.IsHighlight,
|
|
})
|
|
}
|
|
|
|
patents, err := pubsRepo.GetPatents(ctx)
|
|
if err != nil {
|
|
log.Fatalf("Get patents: %v", err)
|
|
}
|
|
type TSPatent struct {
|
|
Category string `json:"category"`
|
|
Title string `json:"title"`
|
|
Inventors string `json:"inventors"`
|
|
ApplicationNo string `json:"applicationNo"`
|
|
ApplicationAt string `json:"applicationAt"`
|
|
RegistrationNo *string `json:"registrationNo,omitempty"`
|
|
RegistrationAt *string `json:"registrationAt,omitempty"`
|
|
Country string `json:"country"`
|
|
PublishedAt string `json:"publishedAt"`
|
|
}
|
|
var tsPatents []TSPatent
|
|
for _, pat := range patents {
|
|
tsPatents = append(tsPatents, TSPatent{
|
|
Category: "patent",
|
|
Title: pat.Title,
|
|
Inventors: pat.Inventors,
|
|
ApplicationNo: pat.ApplicationNo,
|
|
ApplicationAt: pat.ApplicationAt,
|
|
RegistrationNo: pat.RegistrationNo,
|
|
RegistrationAt: pat.RegistrationAt,
|
|
Country: pat.Country,
|
|
PublishedAt: pat.PublishedAt,
|
|
})
|
|
}
|
|
|
|
pubsJSON, err := marshalJSONNoEscapeHTML(tsPubs)
|
|
if err != nil {
|
|
log.Fatalf("Marshal publications failed: %v", err)
|
|
}
|
|
patentsJSON, err := marshalJSONNoEscapeHTML(tsPatents)
|
|
if err != nil {
|
|
log.Fatalf("Marshal patents failed: %v", err)
|
|
}
|
|
pubsTS := fmt.Sprintf("// AUTO-GENERATED by backend/cmd/export-content — DO NOT EDIT BY HAND\nimport { Publication, Patent } from \"./types\";\n\nexport const publications: Publication[] = %s;\n\nexport const patents: Patent[] = %s;\n", string(pubsJSON), string(patentsJSON))
|
|
if err := writeFileSafe(filepath.Join(*targetDir, "publications.ts"), []byte(pubsTS)); err != nil {
|
|
log.Fatalf("Write publications.ts failed: %v", err)
|
|
}
|
|
log.Println("✓ Exported publications.ts (with isHighlight metadata & unescaped HTML entities)")
|
|
|
|
// 4. Lectures
|
|
semesters, err := lecturesRepo.GetSemestersWithCourses(ctx)
|
|
if err != nil {
|
|
log.Fatalf("Get semesters: %v", err)
|
|
}
|
|
type TSCourse struct {
|
|
Code string `json:"code"`
|
|
En string `json:"en"`
|
|
Note *string `json:"note,omitempty"`
|
|
}
|
|
type TSSemester struct {
|
|
Term string `json:"term"`
|
|
Year int `json:"year"`
|
|
Courses []TSCourse `json:"courses"`
|
|
}
|
|
var tsSemesters []TSSemester
|
|
for _, s := range semesters {
|
|
var tsCourses []TSCourse
|
|
for _, c := range s.Courses {
|
|
tsCourses = append(tsCourses, TSCourse{
|
|
Code: c.Code,
|
|
En: c.NameEn,
|
|
Note: c.Note,
|
|
})
|
|
}
|
|
tsSemesters = append(tsSemesters, TSSemester{
|
|
Term: s.Term,
|
|
Year: s.Year,
|
|
Courses: tsCourses,
|
|
})
|
|
}
|
|
lecturesBytes, err := formatTSFile(`import { Semester } from "./types";`, "semesters", "Semester[]", tsSemesters)
|
|
if err != nil {
|
|
log.Fatalf("Format lectures.ts failed: %v", err)
|
|
}
|
|
if err := writeFileSafe(filepath.Join(*targetDir, "lectures.ts"), lecturesBytes); err != nil {
|
|
log.Fatalf("Write lectures.ts failed: %v", err)
|
|
}
|
|
log.Println("✓ Exported lectures.ts")
|
|
|
|
// 5. Standards
|
|
standards, err := standardsRepo.GetStandardsBodiesWithProjects(ctx)
|
|
if err != nil {
|
|
log.Fatalf("Get standards bodies: %v", err)
|
|
}
|
|
type TSDoc struct {
|
|
Title string `json:"title"`
|
|
Ref string `json:"ref"`
|
|
Status string `json:"status"`
|
|
PublishedAt *string `json:"publishedAt,omitempty"`
|
|
}
|
|
type TSStandardProject struct {
|
|
WG string `json:"wg"`
|
|
Name string `json:"name"`
|
|
Documents []TSDoc `json:"documents"`
|
|
}
|
|
type TSStandardsBody struct {
|
|
Org string `json:"org"`
|
|
Full string `json:"full"`
|
|
Scope string `json:"scope"`
|
|
Period string `json:"period"`
|
|
Role *string `json:"role,omitempty"`
|
|
Projects []TSStandardProject `json:"projects"`
|
|
}
|
|
var tsStandards []TSStandardsBody
|
|
for _, b := range standards {
|
|
var tsProjs []TSStandardProject
|
|
for _, p := range b.Projects {
|
|
var tsDocs []TSDoc
|
|
for _, d := range p.Documents {
|
|
tsDocs = append(tsDocs, TSDoc{
|
|
Title: d.Title,
|
|
Ref: d.DocRef,
|
|
Status: d.Status,
|
|
PublishedAt: d.PublishedAt,
|
|
})
|
|
}
|
|
tsProjs = append(tsProjs, TSStandardProject{
|
|
WG: p.WG,
|
|
Name: p.Name,
|
|
Documents: tsDocs,
|
|
})
|
|
}
|
|
tsStandards = append(tsStandards, TSStandardsBody{
|
|
Org: b.Org,
|
|
Full: b.FullName,
|
|
Scope: b.Scope,
|
|
Period: b.Period,
|
|
Role: b.Role,
|
|
Projects: tsProjs,
|
|
})
|
|
}
|
|
standardsBytes, err := formatTSFile(`import { StandardsBody } from "./types";`, "standardsBodies", "StandardsBody[]", tsStandards)
|
|
if err != nil {
|
|
log.Fatalf("Format standards.ts failed: %v", err)
|
|
}
|
|
if err := writeFileSafe(filepath.Join(*targetDir, "standards.ts"), standardsBytes); err != nil {
|
|
log.Fatalf("Write standards.ts failed: %v", err)
|
|
}
|
|
log.Println("✓ Exported standards.ts")
|
|
|
|
log.Println("=== All content successfully exported from SQLite! ===")
|
|
}
|