feat: soporte projects y vaults en registry
Añade tablas projects y vaults a registry.db con FTS5, modelos Go,
parser de project.md y vault.yaml, CRUD completo en store, hashing
determinista, validación, y soporte en el indexer para escanear
projects/{name}/ con sus apps, analysis y vaults anidados.
Migration 010 crea las tablas, triggers FTS5, y columna project_id
en apps/analysis. El indexer preserva records remotos (repo_url) al
reindexar, igual que apps/analysis.
This commit is contained in:
+255
-13
@@ -291,12 +291,12 @@ func (db *DB) InsertApp(a *App) error {
|
||||
INSERT OR REPLACE INTO apps (
|
||||
id, name, lang, domain, description, tags,
|
||||
uses_functions, uses_types, framework, entry_point,
|
||||
documentation, notes, dir_path, content_hash, created_at, updated_at, repo_url
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
documentation, notes, dir_path, content_hash, created_at, updated_at, repo_url, project_id
|
||||
) 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.ContentHash, a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339),
|
||||
a.RepoURL,
|
||||
a.RepoURL, a.ProjectID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -363,7 +363,7 @@ func scanApps(rows interface{ Next() bool; Scan(...any) error }) ([]App, error)
|
||||
&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, &a.ContentHash,
|
||||
&a.RepoURL,
|
||||
&a.RepoURL, &a.ProjectID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning app: %w", err)
|
||||
@@ -380,7 +380,7 @@ func scanApps(rows interface{ Next() bool; Scan(...any) error }) ([]App, error)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Purge deletes all data from functions, types, apps and analysis. Used before re-indexing.
|
||||
// Purge deletes all data from functions, types, apps, analysis, projects and vaults. Used before re-indexing.
|
||||
func (db *DB) Purge() error {
|
||||
if _, err := db.conn.Exec("DELETE FROM functions"); err != nil {
|
||||
return err
|
||||
@@ -391,13 +391,19 @@ func (db *DB) Purge() error {
|
||||
if _, err := db.conn.Exec("DELETE FROM apps"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := db.conn.Exec("DELETE FROM analysis")
|
||||
if _, err := db.conn.Exec("DELETE FROM analysis"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.conn.Exec("DELETE FROM projects"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := db.conn.Exec("DELETE FROM vaults")
|
||||
return err
|
||||
}
|
||||
|
||||
// PurgeLocalOnly deletes functions, types, and only locally-present apps/analysis.
|
||||
// Remote-only records (repo_url set, not in localAppIDs/localAnalysisIDs) are preserved.
|
||||
func (db *DB) PurgeLocalOnly(localAppIDs, localAnalysisIDs map[string]bool) error {
|
||||
// PurgeLocalOnly deletes functions, types, and only locally-present apps/analysis/projects/vaults.
|
||||
// Remote-only records (repo_url set, not in local ID maps) are preserved.
|
||||
func (db *DB) PurgeLocalOnly(localAppIDs, localAnalysisIDs, localProjectIDs map[string]bool) error {
|
||||
if _, err := db.conn.Exec("DELETE FROM functions"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -423,6 +429,19 @@ func (db *DB) PurgeLocalOnly(localAppIDs, localAnalysisIDs map[string]bool) erro
|
||||
if _, err := db.conn.Exec("DELETE FROM analysis WHERE repo_url = '' OR repo_url IS NULL"); err != nil {
|
||||
return err
|
||||
}
|
||||
// Projects: delete locally-scanned, preserve remote-only
|
||||
for id := range localProjectIDs {
|
||||
if _, err := db.conn.Exec("DELETE FROM projects WHERE id = ?", id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := db.conn.Exec("DELETE FROM projects WHERE repo_url = '' OR repo_url IS NULL"); err != nil {
|
||||
return err
|
||||
}
|
||||
// Vaults: always purge and re-insert from vault.yaml
|
||||
if _, err := db.conn.Exec("DELETE FROM vaults"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -446,12 +465,12 @@ func (db *DB) InsertAnalysis(a *Analysis) error {
|
||||
INSERT OR REPLACE INTO analysis (
|
||||
id, name, lang, domain, description, tags,
|
||||
uses_functions, uses_types, framework, entry_point,
|
||||
documentation, notes, repo_url, dir_path, content_hash, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
documentation, notes, repo_url, dir_path, content_hash, created_at, updated_at, project_id
|
||||
) 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.RepoURL, a.DirPath, a.ContentHash,
|
||||
a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339),
|
||||
a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339), a.ProjectID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -528,7 +547,7 @@ func scanAnalysis(rows interface{ Next() bool; Scan(...any) error }) ([]Analysis
|
||||
&a.ID, &a.Name, &a.Lang, &a.Domain, &a.Description, &tagsJSON,
|
||||
&usesFnJSON, &usesTypJSON, &a.Framework, &a.EntryPoint,
|
||||
&a.Documentation, &a.Notes, &a.RepoURL, &a.DirPath, &a.ContentHash,
|
||||
&createdAt, &updatedAt,
|
||||
&createdAt, &updatedAt, &a.ProjectID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning analysis: %w", err)
|
||||
@@ -545,6 +564,229 @@ func scanAnalysis(rows interface{ Next() bool; Scan(...any) error }) ([]Analysis
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// --- Project CRUD ---
|
||||
|
||||
// InsertProject inserts or replaces a project entry.
|
||||
func (db *DB) InsertProject(p *Project) error {
|
||||
now := time.Now().UTC()
|
||||
if p.CreatedAt.IsZero() {
|
||||
p.CreatedAt = now
|
||||
}
|
||||
if p.UpdatedAt.IsZero() {
|
||||
p.UpdatedAt = now
|
||||
}
|
||||
|
||||
_, err := db.conn.Exec(`
|
||||
INSERT OR REPLACE INTO projects (
|
||||
id, name, description, tags, repo_url, dir_path,
|
||||
documentation, notes, content_hash, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
p.ID, p.Name, p.Description, marshalStrings(p.Tags), p.RepoURL, p.DirPath,
|
||||
p.Documentation, p.Notes, p.ContentHash,
|
||||
p.CreatedAt.Format(time.RFC3339), p.UpdatedAt.Format(time.RFC3339),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetProject returns a single project by ID.
|
||||
func (db *DB) GetProject(id string) (*Project, error) {
|
||||
rows, err := db.conn.Query("SELECT * FROM projects WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
ps, err := scanProjects(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ps) == 0 {
|
||||
return nil, fmt.Errorf("project %q not found", id)
|
||||
}
|
||||
return &ps[0], nil
|
||||
}
|
||||
|
||||
// SearchProjects performs FTS search on projects.
|
||||
func (db *DB) SearchProjects(query string) ([]Project, error) {
|
||||
where := []string{}
|
||||
args := []any{}
|
||||
|
||||
if query != "" {
|
||||
where = append(where, "p.id IN (SELECT id FROM projects_fts WHERE projects_fts MATCH ?)")
|
||||
args = append(args, query)
|
||||
}
|
||||
|
||||
sql := "SELECT * FROM projects p"
|
||||
if len(where) > 0 {
|
||||
sql += " WHERE " + strings.Join(where, " AND ")
|
||||
}
|
||||
sql += " ORDER BY p.name"
|
||||
|
||||
rows, err := db.conn.Query(sql, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search projects: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return scanProjects(rows)
|
||||
}
|
||||
|
||||
// ListAllProjects returns all project entries.
|
||||
func (db *DB) ListAllProjects() ([]Project, error) {
|
||||
return db.SearchProjects("")
|
||||
}
|
||||
|
||||
// GetProjectApps returns all apps belonging to a project.
|
||||
func (db *DB) GetProjectApps(projectID string) ([]App, error) {
|
||||
rows, err := db.conn.Query("SELECT * FROM apps WHERE project_id = ? ORDER BY name", projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanApps(rows)
|
||||
}
|
||||
|
||||
// GetProjectAnalysis returns all analysis entries belonging to a project.
|
||||
func (db *DB) GetProjectAnalysis(projectID string) ([]Analysis, error) {
|
||||
rows, err := db.conn.Query("SELECT * FROM analysis WHERE project_id = ? ORDER BY name", projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanAnalysis(rows)
|
||||
}
|
||||
|
||||
func scanProjects(rows interface{ Next() bool; Scan(...any) error }) ([]Project, error) {
|
||||
var result []Project
|
||||
for rows.Next() {
|
||||
var p Project
|
||||
var tagsJSON string
|
||||
var createdAt, updatedAt string
|
||||
|
||||
err := rows.Scan(
|
||||
&p.ID, &p.Name, &p.Description, &tagsJSON, &p.RepoURL, &p.DirPath,
|
||||
&p.Documentation, &p.Notes, &p.ContentHash, &createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning project: %w", err)
|
||||
}
|
||||
|
||||
p.Tags = unmarshalStrings(tagsJSON)
|
||||
p.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
p.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
|
||||
result = append(result, p)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// --- Vault CRUD ---
|
||||
|
||||
// InsertVault inserts or replaces a vault entry.
|
||||
func (db *DB) InsertVault(v *Vault) error {
|
||||
now := time.Now().UTC()
|
||||
if v.CreatedAt.IsZero() {
|
||||
v.CreatedAt = now
|
||||
}
|
||||
if v.UpdatedAt.IsZero() {
|
||||
v.UpdatedAt = now
|
||||
}
|
||||
|
||||
sym := 0
|
||||
if v.Symlink {
|
||||
sym = 1
|
||||
}
|
||||
|
||||
_, err := db.conn.Exec(`
|
||||
INSERT OR REPLACE INTO vaults (
|
||||
id, name, project_id, description, path, symlink, tags,
|
||||
content_hash, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
v.ID, v.Name, v.ProjectID, v.Description, v.Path, sym, marshalStrings(v.Tags),
|
||||
v.ContentHash, v.CreatedAt.Format(time.RFC3339), v.UpdatedAt.Format(time.RFC3339),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetVault returns a single vault by ID.
|
||||
func (db *DB) GetVault(id string) (*Vault, error) {
|
||||
rows, err := db.conn.Query("SELECT * FROM vaults WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
vs, err := scanVaults(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(vs) == 0 {
|
||||
return nil, fmt.Errorf("vault %q not found", id)
|
||||
}
|
||||
return &vs[0], nil
|
||||
}
|
||||
|
||||
// SearchVaults performs search on vaults with optional project filter.
|
||||
func (db *DB) SearchVaults(query, projectID string) ([]Vault, error) {
|
||||
where := []string{}
|
||||
args := []any{}
|
||||
|
||||
if query != "" {
|
||||
where = append(where, "name LIKE ? OR description LIKE ?")
|
||||
q := "%" + query + "%"
|
||||
args = append(args, q, q)
|
||||
}
|
||||
if projectID != "" {
|
||||
where = append(where, "project_id = ?")
|
||||
args = append(args, projectID)
|
||||
}
|
||||
|
||||
sql := "SELECT * FROM vaults"
|
||||
if len(where) > 0 {
|
||||
sql += " WHERE " + strings.Join(where, " AND ")
|
||||
}
|
||||
sql += " ORDER BY name"
|
||||
|
||||
rows, err := db.conn.Query(sql, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search vaults: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return scanVaults(rows)
|
||||
}
|
||||
|
||||
// GetProjectVaults returns all vaults belonging to a project.
|
||||
func (db *DB) GetProjectVaults(projectID string) ([]Vault, error) {
|
||||
return db.SearchVaults("", projectID)
|
||||
}
|
||||
|
||||
func scanVaults(rows interface{ Next() bool; Scan(...any) error }) ([]Vault, error) {
|
||||
var result []Vault
|
||||
for rows.Next() {
|
||||
var v Vault
|
||||
var tagsJSON string
|
||||
var createdAt, updatedAt string
|
||||
var sym int
|
||||
|
||||
err := rows.Scan(
|
||||
&v.ID, &v.Name, &v.ProjectID, &v.Description, &v.Path, &sym, &tagsJSON,
|
||||
&v.ContentHash, &createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning vault: %w", err)
|
||||
}
|
||||
|
||||
v.Symlink = sym == 1
|
||||
v.Tags = unmarshalStrings(tagsJSON)
|
||||
v.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
v.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
|
||||
result = append(result, v)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func scanFunctions(rows interface{ Next() bool; Scan(...any) error }) ([]Function, error) {
|
||||
var result []Function
|
||||
for rows.Next() {
|
||||
|
||||
Reference in New Issue
Block a user