feat: tabla apps en registry — modelo, parser, indexer y CLI
Agrega soporte completo para indexar aplicaciones del directorio apps/. Cada app tiene un descriptor app.md con frontmatter YAML que el indexer recoge automaticamente. Incluye migracion 004, modelo App, ParseAppMD, ValidateApp, store CRUD con FTS5, y soporte en fn list/search/show. Crea descriptores app.md para docker_tui, pipeline_launcher y metabase_registry.
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: docker_tui
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
description: "TUI interactiva para gestion de contenedores, imagenes, volumenes y redes Docker."
|
||||||
|
tags: [docker, tui, bubbletea, containers]
|
||||||
|
uses_functions:
|
||||||
|
- docker_pull_image_go_infra
|
||||||
|
- docker_list_containers_go_infra
|
||||||
|
- docker_remove_container_go_infra
|
||||||
|
- docker_stop_container_go_infra
|
||||||
|
- docker_start_container_go_infra
|
||||||
|
- docker_list_images_go_infra
|
||||||
|
- docker_remove_image_go_infra
|
||||||
|
- docker_remove_network_go_infra
|
||||||
|
- docker_create_network_go_infra
|
||||||
|
- docker_inspect_container_go_infra
|
||||||
|
- docker_run_container_go_infra
|
||||||
|
- docker_container_logs_go_infra
|
||||||
|
uses_types: []
|
||||||
|
framework: bubbletea
|
||||||
|
entry_point: "main.go"
|
||||||
|
dir_path: "apps/docker_tui"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Aplicacion TUI con pestanas para contenedores, imagenes, volumenes, redes y compose. Construida con Bubble Tea (Charmbracelet).
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: metabase_registry
|
||||||
|
lang: py
|
||||||
|
domain: analytics
|
||||||
|
description: "Setup y dashboards automaticos de Metabase para visualizar metricas del fn-registry."
|
||||||
|
tags: [metabase, dashboard, analytics, visualization]
|
||||||
|
uses_functions:
|
||||||
|
- metabase_auth_py_infra
|
||||||
|
- metabase_create_card_py_infra
|
||||||
|
- metabase_create_dashboard_py_infra
|
||||||
|
- metabase_update_dashboard_py_infra
|
||||||
|
- metabase_list_databases_py_infra
|
||||||
|
- metabase_add_database_py_infra
|
||||||
|
- metabase_list_dashboards_py_infra
|
||||||
|
- metabase_create_user_py_infra
|
||||||
|
uses_types: []
|
||||||
|
framework: httpx
|
||||||
|
entry_point: "main.py"
|
||||||
|
dir_path: "apps/metabase_registry"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Scripts Python que conectan con la API REST de Metabase para crear datasources, cards SQL y dashboards automaticamente. Usa las funciones del paquete python/functions/metabase/ del registry. Credenciales en .env local.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
name: pipeline_launcher
|
||||||
|
lang: go
|
||||||
|
domain: tools
|
||||||
|
description: "TUI para lanzar y monitorear pipelines del fn-registry con historial de ejecuciones."
|
||||||
|
tags: [pipeline, tui, bubbletea, runner, launcher]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
framework: bubbletea
|
||||||
|
entry_point: "main.go"
|
||||||
|
dir_path: "apps/pipeline_launcher"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Aplicacion TUI que lista pipelines con tag `launcher` del registry, permite ejecutarlos y muestra historial de ejecuciones desde operations.db.
|
||||||
+74
-4
@@ -102,7 +102,10 @@ func cmdIndex() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Indexed %d functions, %d types\n", result.Functions, result.Types)
|
// Flush WAL to main db file so external readers (e.g. Metabase) see changes.
|
||||||
|
db.WalCheckpoint()
|
||||||
|
|
||||||
|
fmt.Printf("Indexed %d functions, %d types, %d apps\n", result.Functions, result.Types, result.Apps)
|
||||||
for _, e := range result.ValidationErrors {
|
for _, e := range result.ValidationErrors {
|
||||||
fmt.Fprintf(os.Stderr, " INVALID: %s\n", e)
|
fmt.Fprintf(os.Stderr, " INVALID: %s\n", e)
|
||||||
}
|
}
|
||||||
@@ -151,7 +154,13 @@ func cmdSearch(args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(fns) == 0 && len(types) == 0 {
|
apps, err := db.SearchApps(query, lang, domain)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fns) == 0 && len(types) == 0 && len(apps) == 0 {
|
||||||
fmt.Println("No results.")
|
fmt.Println("No results.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -174,6 +183,16 @@ func cmdSearch(args []string) {
|
|||||||
fmt.Fprintf(w, "%s\t%s\t%s\n", t.Algebraic, t.ID, desc)
|
fmt.Fprintf(w, "%s\t%s\t%s\n", t.Algebraic, t.ID, desc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(apps) > 0 {
|
||||||
|
if len(fns) > 0 || len(types) > 0 {
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w, "APP\tID\tLANG\tDESCRIPTION")
|
||||||
|
for _, a := range apps {
|
||||||
|
desc := truncate(a.Description, 60)
|
||||||
|
fmt.Fprintf(w, "app\t%s\t%s\t%s\n", a.ID, a.Lang, desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
w.Flush()
|
w.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +231,12 @@ func cmdList(args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apps, err := db.SearchApps("", lang, domain)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
if len(fns) > 0 {
|
if len(fns) > 0 {
|
||||||
fmt.Fprintln(w, "KIND\tID\tPURITY\tVERSION\tDOMAIN")
|
fmt.Fprintln(w, "KIND\tID\tPURITY\tVERSION\tDOMAIN")
|
||||||
@@ -228,7 +253,16 @@ func cmdList(args []string) {
|
|||||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", t.Algebraic, t.ID, t.Version, t.Domain)
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", t.Algebraic, t.ID, t.Version, t.Domain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(fns) == 0 && len(types) == 0 {
|
if len(apps) > 0 {
|
||||||
|
if len(fns) > 0 || len(types) > 0 {
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w, "APP\tID\tLANG\tDOMAIN")
|
||||||
|
for _, a := range apps {
|
||||||
|
fmt.Fprintf(w, "app\t%s\t%s\t%s\n", a.ID, a.Lang, a.Domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(fns) == 0 && len(types) == 0 && len(apps) == 0 {
|
||||||
fmt.Println("Registry is empty. Run 'fn index' first.")
|
fmt.Println("Registry is empty. Run 'fn index' first.")
|
||||||
}
|
}
|
||||||
w.Flush()
|
w.Flush()
|
||||||
@@ -258,6 +292,12 @@ func cmdShow(args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a, errA := db.GetApp(id)
|
||||||
|
if errA == nil {
|
||||||
|
printApp(a)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "not found: %s\n", id)
|
fmt.Fprintf(os.Stderr, "not found: %s\n", id)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -342,6 +382,34 @@ func printType(t *registry.Type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printApp(a *registry.App) {
|
||||||
|
fmt.Printf("ID: %s\n", a.ID)
|
||||||
|
fmt.Printf("Name: %s\n", a.Name)
|
||||||
|
fmt.Printf("Lang: %s\n", a.Lang)
|
||||||
|
fmt.Printf("Domain: %s\n", a.Domain)
|
||||||
|
fmt.Printf("Description: %s\n", a.Description)
|
||||||
|
fmt.Printf("Tags: %s\n", strings.Join(a.Tags, ", "))
|
||||||
|
fmt.Printf("Dir: %s\n", a.DirPath)
|
||||||
|
if a.Framework != "" {
|
||||||
|
fmt.Printf("Framework: %s\n", a.Framework)
|
||||||
|
}
|
||||||
|
if a.EntryPoint != "" {
|
||||||
|
fmt.Printf("Entry point: %s\n", a.EntryPoint)
|
||||||
|
}
|
||||||
|
if len(a.UsesFunctions) > 0 {
|
||||||
|
fmt.Printf("Uses fns: %s\n", strings.Join(a.UsesFunctions, ", "))
|
||||||
|
}
|
||||||
|
if len(a.UsesTypes) > 0 {
|
||||||
|
fmt.Printf("Uses types: %s\n", strings.Join(a.UsesTypes, ", "))
|
||||||
|
}
|
||||||
|
if a.Notes != "" {
|
||||||
|
fmt.Printf("\nNotes:\n%s\n", a.Notes)
|
||||||
|
}
|
||||||
|
if a.Documentation != "" {
|
||||||
|
fmt.Printf("\nDocumentation:\n%s\n", a.Documentation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- add ---
|
// --- add ---
|
||||||
|
|
||||||
func cmdAdd(args []string) {
|
func cmdAdd(args []string) {
|
||||||
@@ -367,8 +435,10 @@ func cmdAdd(args []string) {
|
|||||||
templatePath = filepath.Join(r, "docs", "templates", "pipeline.md")
|
templatePath = filepath.Join(r, "docs", "templates", "pipeline.md")
|
||||||
case "component":
|
case "component":
|
||||||
templatePath = filepath.Join(r, "docs", "templates", "component.md")
|
templatePath = filepath.Join(r, "docs", "templates", "component.md")
|
||||||
|
case "app":
|
||||||
|
templatePath = filepath.Join(r, "docs", "templates", "app.md")
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "unknown kind: %s (use function, pipeline, or component)\n", kind)
|
fmt.Fprintf(os.Stderr, "unknown kind: %s (use function, pipeline, component, or app)\n", kind)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
name: my_app
|
||||||
|
lang: go
|
||||||
|
domain: tools
|
||||||
|
description: "Descripcion breve de la aplicacion."
|
||||||
|
tags: []
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
framework: ""
|
||||||
|
entry_point: "main.go"
|
||||||
|
dir_path: "apps/my_app"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Notas adicionales sobre la aplicacion.
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
type IndexResult struct {
|
type IndexResult struct {
|
||||||
Functions int
|
Functions int
|
||||||
Types int
|
Types int
|
||||||
|
Apps int
|
||||||
ValidationErrors []string
|
ValidationErrors []string
|
||||||
Errors []string
|
Errors []string
|
||||||
}
|
}
|
||||||
@@ -76,6 +77,28 @@ func Index(db *DB, root string) (*IndexResult, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse apps from apps/*/app.md
|
||||||
|
var apps []*App
|
||||||
|
appsDir := filepath.Join(root, "apps")
|
||||||
|
if fi, err := os.Stat(appsDir); err == nil && fi.IsDir() {
|
||||||
|
entries, _ := os.ReadDir(appsDir)
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
appMD := filepath.Join(appsDir, e.Name(), "app.md")
|
||||||
|
if _, err := os.Stat(appMD); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a, err := ParseAppMD(appMD, root)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", appMD, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
apps = append(apps, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build known ID sets
|
// Build known ID sets
|
||||||
knownFunctions := make(map[string]bool, len(functions))
|
knownFunctions := make(map[string]bool, len(functions))
|
||||||
for _, f := range functions {
|
for _, f := range functions {
|
||||||
@@ -111,6 +134,18 @@ func Index(db *DB, root string) (*IndexResult, error) {
|
|||||||
result.Functions++
|
result.Functions++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, a := range apps {
|
||||||
|
if verr := ValidateApp(a, knownFunctions, knownTypes); verr != nil {
|
||||||
|
result.ValidationErrors = append(result.ValidationErrors, verr.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := db.InsertApp(a); err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("insert %s: %v", a.ID, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.Apps++
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
-- Apps table: applications that consume functions/types from the registry.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS apps (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
lang TEXT NOT NULL,
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
tags TEXT NOT NULL DEFAULT '[]',
|
||||||
|
uses_functions TEXT NOT NULL DEFAULT '[]',
|
||||||
|
uses_types TEXT NOT NULL DEFAULT '[]',
|
||||||
|
framework TEXT NOT NULL DEFAULT '',
|
||||||
|
entry_point TEXT NOT NULL DEFAULT '',
|
||||||
|
documentation TEXT NOT NULL DEFAULT '',
|
||||||
|
notes TEXT NOT NULL DEFAULT '',
|
||||||
|
dir_path TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS apps_fts USING fts5(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
domain,
|
||||||
|
documentation,
|
||||||
|
notes,
|
||||||
|
content='apps',
|
||||||
|
content_rowid='rowid'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS apps_ai AFTER INSERT ON apps BEGIN
|
||||||
|
INSERT INTO apps_fts(rowid, id, name, description, tags, domain, documentation, notes)
|
||||||
|
VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.domain, new.documentation, new.notes);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS apps_ad AFTER DELETE ON apps BEGIN
|
||||||
|
INSERT INTO apps_fts(apps_fts, rowid, id, name, description, tags, domain, documentation, notes)
|
||||||
|
VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.domain, old.documentation, old.notes);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS apps_au AFTER UPDATE ON apps BEGIN
|
||||||
|
INSERT INTO apps_fts(apps_fts, rowid, id, name, description, tags, domain, documentation, notes)
|
||||||
|
VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.domain, old.documentation, old.notes);
|
||||||
|
INSERT INTO apps_fts(rowid, id, name, description, tags, domain, documentation, notes)
|
||||||
|
VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.domain, new.documentation, new.notes);
|
||||||
|
END;
|
||||||
@@ -94,6 +94,25 @@ type Type struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// App represents an entry in the apps table.
|
||||||
|
type App struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Lang string `json:"lang"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
UsesFunctions []string `json:"uses_functions"`
|
||||||
|
UsesTypes []string `json:"uses_types"`
|
||||||
|
Framework string `json:"framework"`
|
||||||
|
EntryPoint string `json:"entry_point"`
|
||||||
|
Documentation string `json:"documentation"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
DirPath string `json:"dir_path"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
// ProposalKind classifies a proposal.
|
// ProposalKind classifies a proposal.
|
||||||
type ProposalKind string
|
type ProposalKind string
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,20 @@ type rawType struct {
|
|||||||
FilePath string `yaml:"file_path"`
|
FilePath string `yaml:"file_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rawApp mirrors the YAML frontmatter of an app .md file.
|
||||||
|
type rawApp struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Lang string `yaml:"lang"`
|
||||||
|
Domain string `yaml:"domain"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Tags []string `yaml:"tags"`
|
||||||
|
UsesFunctions []string `yaml:"uses_functions"`
|
||||||
|
UsesTypes []string `yaml:"uses_types"`
|
||||||
|
Framework string `yaml:"framework"`
|
||||||
|
EntryPoint string `yaml:"entry_point"`
|
||||||
|
DirPath string `yaml:"dir_path"`
|
||||||
|
}
|
||||||
|
|
||||||
// extractFrontmatter splits a .md file into YAML frontmatter and body.
|
// extractFrontmatter splits a .md file into YAML frontmatter and body.
|
||||||
func extractFrontmatter(data []byte) ([]byte, []byte, error) {
|
func extractFrontmatter(data []byte) ([]byte, []byte, error) {
|
||||||
content := data
|
content := data
|
||||||
@@ -198,6 +212,51 @@ func ParseTypeMD(path string, root string) (*Type, error) {
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseAppMD parses an app .md file into an App.
|
||||||
|
func ParseAppMD(path string, root string) (*App, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fm, body, err := extractFrontmatter(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw rawApp
|
||||||
|
if err := yaml.Unmarshal(fm, &raw); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing YAML in %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw.Name == "" {
|
||||||
|
return nil, fmt.Errorf("%s: name is required", path)
|
||||||
|
}
|
||||||
|
if raw.Description == "" {
|
||||||
|
return nil, fmt.Errorf("%s: description is required", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
sections := extractSections(body)
|
||||||
|
|
||||||
|
a := &App{
|
||||||
|
ID: GenerateID(raw.Name, raw.Lang, raw.Domain),
|
||||||
|
Name: raw.Name,
|
||||||
|
Lang: raw.Lang,
|
||||||
|
Domain: raw.Domain,
|
||||||
|
Description: raw.Description,
|
||||||
|
Tags: raw.Tags,
|
||||||
|
UsesFunctions: raw.UsesFunctions,
|
||||||
|
UsesTypes: raw.UsesTypes,
|
||||||
|
Framework: raw.Framework,
|
||||||
|
EntryPoint: raw.EntryPoint,
|
||||||
|
Documentation: sections.documentation,
|
||||||
|
Notes: sections.notes,
|
||||||
|
DirPath: raw.DirPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
// bodySections holds the extracted sections from a .md body.
|
// bodySections holds the extracted sections from a .md body.
|
||||||
type bodySections struct {
|
type bodySections struct {
|
||||||
example string // content under ## Ejemplo
|
example string // content under ## Ejemplo
|
||||||
|
|||||||
+108
-2
@@ -261,12 +261,118 @@ func (db *DB) DeleteType(id string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Purge deletes all data from both tables. Used before re-indexing.
|
// InsertApp inserts or replaces an app entry.
|
||||||
|
func (db *DB) InsertApp(a *App) error {
|
||||||
|
now := time.Now().UTC().Format(time.RFC3339)
|
||||||
|
if a.CreatedAt.IsZero() {
|
||||||
|
a.CreatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
a.UpdatedAt = time.Now().UTC()
|
||||||
|
|
||||||
|
if a.ID == "" {
|
||||||
|
a.ID = GenerateID(a.Name, a.Lang, a.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := db.conn.Exec(`
|
||||||
|
INSERT OR REPLACE INTO apps (
|
||||||
|
id, name, lang, domain, description, tags,
|
||||||
|
uses_functions, uses_types, framework, entry_point,
|
||||||
|
documentation, notes, dir_path, created_at, updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
a.ID, a.Name, a.Lang, a.Domain, a.Description, marshalStrings(a.Tags),
|
||||||
|
marshalStrings(a.UsesFunctions), marshalStrings(a.UsesTypes), a.Framework, a.EntryPoint,
|
||||||
|
a.Documentation, a.Notes, a.DirPath, a.CreatedAt.Format(time.RFC3339), now,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApp returns a single app by ID.
|
||||||
|
func (db *DB) GetApp(id string) (*App, error) {
|
||||||
|
rows, err := db.conn.Query("SELECT * FROM apps WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
apps, err := scanApps(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(apps) == 0 {
|
||||||
|
return nil, fmt.Errorf("app %q not found", id)
|
||||||
|
}
|
||||||
|
return &apps[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchApps performs FTS search on apps with optional filters.
|
||||||
|
func (db *DB) SearchApps(query string, lang, domain string) ([]App, error) {
|
||||||
|
where := []string{}
|
||||||
|
args := []any{}
|
||||||
|
|
||||||
|
if query != "" {
|
||||||
|
where = append(where, "a.id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH ?)")
|
||||||
|
args = append(args, query)
|
||||||
|
}
|
||||||
|
if lang != "" {
|
||||||
|
where = append(where, "a.lang = ?")
|
||||||
|
args = append(args, lang)
|
||||||
|
}
|
||||||
|
if domain != "" {
|
||||||
|
where = append(where, "a.domain = ?")
|
||||||
|
args = append(args, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := "SELECT * FROM apps a"
|
||||||
|
if len(where) > 0 {
|
||||||
|
sql += " WHERE " + strings.Join(where, " AND ")
|
||||||
|
}
|
||||||
|
sql += " ORDER BY a.name"
|
||||||
|
|
||||||
|
rows, err := db.conn.Query(sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("search apps: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
return scanApps(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanApps(rows interface{ Next() bool; Scan(...any) error }) ([]App, error) {
|
||||||
|
var result []App
|
||||||
|
for rows.Next() {
|
||||||
|
var a App
|
||||||
|
var tagsJSON, usesFnJSON, usesTypJSON string
|
||||||
|
var createdAt, updatedAt string
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&a.ID, &a.Name, &a.Lang, &a.Domain, &a.Description, &tagsJSON,
|
||||||
|
&usesFnJSON, &usesTypJSON, &a.Framework, &a.EntryPoint,
|
||||||
|
&a.Documentation, &a.Notes, &a.DirPath, &createdAt, &updatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("scanning app: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Tags = unmarshalStrings(tagsJSON)
|
||||||
|
a.UsesFunctions = unmarshalStrings(usesFnJSON)
|
||||||
|
a.UsesTypes = unmarshalStrings(usesTypJSON)
|
||||||
|
a.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||||
|
a.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||||
|
|
||||||
|
result = append(result, a)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purge deletes all data from functions, types and apps. Used before re-indexing.
|
||||||
func (db *DB) Purge() error {
|
func (db *DB) Purge() error {
|
||||||
if _, err := db.conn.Exec("DELETE FROM functions"); err != nil {
|
if _, err := db.conn.Exec("DELETE FROM functions"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err := db.conn.Exec("DELETE FROM types")
|
if _, err := db.conn.Exec("DELETE FROM types"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := db.conn.Exec("DELETE FROM apps")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -161,6 +161,44 @@ func ValidateProposal(p *Proposal) *ValidationError {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateApp checks integrity rules for apps.
|
||||||
|
func ValidateApp(a *App, knownFunctions, knownTypes map[string]bool) *ValidationError {
|
||||||
|
var errs []string
|
||||||
|
|
||||||
|
if a.Name == "" {
|
||||||
|
errs = append(errs, "name is required")
|
||||||
|
}
|
||||||
|
if a.Lang == "" {
|
||||||
|
errs = append(errs, "lang is required")
|
||||||
|
}
|
||||||
|
if a.Domain == "" {
|
||||||
|
errs = append(errs, "domain is required")
|
||||||
|
}
|
||||||
|
if a.Description == "" {
|
||||||
|
errs = append(errs, "description is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.DirPath != "" && strings.HasPrefix(a.DirPath, "/") {
|
||||||
|
errs = append(errs, "dir_path must be relative to registry root")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ref := range a.UsesFunctions {
|
||||||
|
if !knownFunctions[ref] {
|
||||||
|
errs = append(errs, fmt.Sprintf("uses_functions references unknown function: %s", ref))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ref := range a.UsesTypes {
|
||||||
|
if !knownTypes[ref] {
|
||||||
|
errs = append(errs, fmt.Sprintf("uses_types references unknown type: %s", ref))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return &ValidationError{ID: a.ID, Errors: errs}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateType checks integrity rules for types.
|
// ValidateType checks integrity rules for types.
|
||||||
func ValidateType(t *Type, knownTypes map[string]bool) *ValidationError {
|
func ValidateType(t *Type, knownTypes map[string]bool) *ValidationError {
|
||||||
var errs []string
|
var errs []string
|
||||||
|
|||||||
Reference in New Issue
Block a user