Changeset 0.17.3 (#78)
This commit is contained in:
@@ -19,31 +19,33 @@ func NewNoteStore() *NoteStore { return &NoteStore{} }
|
||||
|
||||
func (s *NoteStore) Create(ctx context.Context, n *models.Note) error {
|
||||
return DB.QueryRowContext(ctx, `
|
||||
INSERT INTO notes (user_id, title, content, folder_path, tags, metadata, source_channel_id, team_id)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
||||
INSERT INTO notes (user_id, title, content, folder_path, tags, metadata, source_channel_id, source_message_id, team_id)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
||||
RETURNING id, created_at, updated_at`,
|
||||
n.UserID, n.Title, n.Content, n.FolderPath,
|
||||
pq.Array(n.Tags), ToJSON(n.Metadata),
|
||||
models.NullString(n.SourceChannelID), models.NullString(n.TeamID),
|
||||
models.NullString(n.SourceChannelID), models.NullString(n.SourceMessageID),
|
||||
models.NullString(n.TeamID),
|
||||
).Scan(&n.ID, &n.CreatedAt, &n.UpdatedAt)
|
||||
}
|
||||
|
||||
func (s *NoteStore) GetByID(ctx context.Context, id string) (*models.Note, error) {
|
||||
var n models.Note
|
||||
var sourceChannelID, teamID sql.NullString
|
||||
var sourceChannelID, sourceMessageID, teamID sql.NullString
|
||||
var metadataJSON []byte
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, user_id, title, content, folder_path, tags, metadata,
|
||||
source_channel_id, team_id, created_at, updated_at
|
||||
source_channel_id, source_message_id, team_id, created_at, updated_at
|
||||
FROM notes WHERE id = $1`, id).Scan(
|
||||
&n.ID, &n.UserID, &n.Title, &n.Content, &n.FolderPath,
|
||||
pq.Array(&n.Tags), &metadataJSON,
|
||||
&sourceChannelID, &teamID, &n.CreatedAt, &n.UpdatedAt,
|
||||
&sourceChannelID, &sourceMessageID, &teamID, &n.CreatedAt, &n.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n.SourceChannelID = NullableStringPtr(sourceChannelID)
|
||||
n.SourceMessageID = NullableStringPtr(sourceMessageID)
|
||||
n.TeamID = NullableStringPtr(teamID)
|
||||
json.Unmarshal(metadataJSON, &n.Metadata)
|
||||
return &n, nil
|
||||
@@ -77,7 +79,7 @@ func (s *NoteStore) Delete(ctx context.Context, id string) error {
|
||||
|
||||
func (s *NoteStore) ListForUser(ctx context.Context, userID string, opts store.NoteListOptions) ([]models.Note, int, error) {
|
||||
b := NewSelect(
|
||||
"id, user_id, title, content, folder_path, tags, metadata, source_channel_id, team_id, created_at, updated_at",
|
||||
"id, user_id, title, content, folder_path, tags, metadata, source_channel_id, source_message_id, team_id, created_at, updated_at",
|
||||
"notes",
|
||||
).Where("user_id = ?", userID)
|
||||
|
||||
@@ -111,15 +113,16 @@ func (s *NoteStore) ListForUser(ctx context.Context, userID string, opts store.N
|
||||
var result []models.Note
|
||||
for rows.Next() {
|
||||
var n models.Note
|
||||
var sourceChannelID, teamID sql.NullString
|
||||
var sourceChannelID, sourceMessageID, teamID sql.NullString
|
||||
var metadataJSON []byte
|
||||
err := rows.Scan(&n.ID, &n.UserID, &n.Title, &n.Content, &n.FolderPath,
|
||||
pq.Array(&n.Tags), &metadataJSON,
|
||||
&sourceChannelID, &teamID, &n.CreatedAt, &n.UpdatedAt)
|
||||
&sourceChannelID, &sourceMessageID, &teamID, &n.CreatedAt, &n.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
n.SourceChannelID = NullableStringPtr(sourceChannelID)
|
||||
n.SourceMessageID = NullableStringPtr(sourceMessageID)
|
||||
n.TeamID = NullableStringPtr(teamID)
|
||||
json.Unmarshal(metadataJSON, &n.Metadata)
|
||||
result = append(result, n)
|
||||
@@ -131,7 +134,7 @@ func (s *NoteStore) Search(ctx context.Context, userID, query string, opts store
|
||||
tsQuery := strings.Join(strings.Fields(query), " & ")
|
||||
|
||||
b := NewSelect(
|
||||
"id, user_id, title, content, folder_path, tags, metadata, source_channel_id, team_id, created_at, updated_at",
|
||||
"id, user_id, title, content, folder_path, tags, metadata, source_channel_id, source_message_id, team_id, created_at, updated_at",
|
||||
"notes",
|
||||
).Where("user_id = ?", userID).Where("search_vector @@ to_tsquery('english', ?)", tsQuery)
|
||||
b.OrderBy("ts_rank(search_vector, to_tsquery('english', '"+tsQuery+"'))", "DESC")
|
||||
@@ -152,15 +155,16 @@ func (s *NoteStore) Search(ctx context.Context, userID, query string, opts store
|
||||
var result []models.Note
|
||||
for rows.Next() {
|
||||
var n models.Note
|
||||
var sourceChannelID, teamID sql.NullString
|
||||
var sourceChannelID, sourceMessageID, teamID sql.NullString
|
||||
var metadataJSON []byte
|
||||
err := rows.Scan(&n.ID, &n.UserID, &n.Title, &n.Content, &n.FolderPath,
|
||||
pq.Array(&n.Tags), &metadataJSON,
|
||||
&sourceChannelID, &teamID, &n.CreatedAt, &n.UpdatedAt)
|
||||
&sourceChannelID, &sourceMessageID, &teamID, &n.CreatedAt, &n.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
n.SourceChannelID = NullableStringPtr(sourceChannelID)
|
||||
n.SourceMessageID = NullableStringPtr(sourceMessageID)
|
||||
n.TeamID = NullableStringPtr(teamID)
|
||||
json.Unmarshal(metadataJSON, &n.Metadata)
|
||||
result = append(result, n)
|
||||
@@ -189,3 +193,29 @@ func (s *NoteStore) BulkDelete(ctx context.Context, ids []string, userID string)
|
||||
n, _ := result.RowsAffected()
|
||||
return int(n), nil
|
||||
}
|
||||
|
||||
func (s *NoteStore) SearchTitles(ctx context.Context, userID, query string, limit int) ([]models.Note, error) {
|
||||
if limit <= 0 || limit > 50 {
|
||||
limit = 10
|
||||
}
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT id, title, folder_path
|
||||
FROM notes
|
||||
WHERE user_id = $1 AND title ILIKE '%' || $2 || '%'
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT $3`, userID, query, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []models.Note
|
||||
for rows.Next() {
|
||||
var n models.Note
|
||||
if err := rows.Scan(&n.ID, &n.Title, &n.FolderPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, n)
|
||||
}
|
||||
return results, rows.Err()
|
||||
}
|
||||
|
||||
179
server/store/postgres/note_link.go
Normal file
179
server/store/postgres/note_link.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type NoteLinkStore struct{}
|
||||
|
||||
func NewNoteLinkStore() *NoteLinkStore { return &NoteLinkStore{} }
|
||||
|
||||
func (s *NoteLinkStore) ReplaceLinks(ctx context.Context, sourceNoteID string, links []models.NoteLink) error {
|
||||
tx, err := DB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Delete existing links for this source note
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
"DELETE FROM note_links WHERE source_note_id = $1", sourceNoteID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert new links
|
||||
if len(links) > 0 {
|
||||
stmt, err := tx.PrepareContext(ctx, `
|
||||
INSERT INTO note_links (source_note_id, target_note_id, target_title, display_text, is_transclusion)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (source_note_id, target_title) DO NOTHING`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for _, l := range links {
|
||||
var targetID interface{}
|
||||
if l.TargetNoteID != nil {
|
||||
targetID = *l.TargetNoteID
|
||||
}
|
||||
if _, err := stmt.ExecContext(ctx,
|
||||
sourceNoteID, targetID, l.TargetTitle, l.DisplayText, l.IsTransclusion,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (s *NoteLinkStore) ResolveByTitle(ctx context.Context, userID, targetNoteID, title string) error {
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
UPDATE note_links
|
||||
SET target_note_id = $1
|
||||
WHERE target_note_id IS NULL
|
||||
AND LOWER(target_title) = LOWER($2)
|
||||
AND source_note_id IN (SELECT id FROM notes WHERE user_id = $3)`,
|
||||
targetNoteID, title, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *NoteLinkStore) Backlinks(ctx context.Context, noteID string) ([]models.NoteLinkResult, error) {
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT n.id, n.title, n.folder_path, n.updated_at, COALESCE(nl.display_text, '')
|
||||
FROM note_links nl
|
||||
JOIN notes n ON n.id = nl.source_note_id
|
||||
WHERE nl.target_note_id = $1
|
||||
ORDER BY n.updated_at DESC`, noteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []models.NoteLinkResult
|
||||
for rows.Next() {
|
||||
var r models.NoteLinkResult
|
||||
if err := rows.Scan(&r.SourceNoteID, &r.Title, &r.FolderPath,
|
||||
&r.UpdatedAt, &r.DisplayText); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, r)
|
||||
}
|
||||
return results, rows.Err()
|
||||
}
|
||||
|
||||
func (s *NoteLinkStore) Graph(ctx context.Context, userID string) (*models.NoteGraph, error) {
|
||||
graph := &models.NoteGraph{
|
||||
Nodes: make([]models.NoteGraphNode, 0),
|
||||
Edges: make([]models.NoteGraphEdge, 0),
|
||||
Unresolved: make([]models.NoteGraphDangling, 0),
|
||||
}
|
||||
|
||||
// Nodes: all user's notes with link counts
|
||||
nodeRows, err := DB.QueryContext(ctx, `
|
||||
SELECT n.id, n.title, n.folder_path, n.tags, n.updated_at::text,
|
||||
COALESCE(outbound.cnt, 0) + COALESCE(inbound.cnt, 0) AS link_count
|
||||
FROM notes n
|
||||
LEFT JOIN (
|
||||
SELECT source_note_id, COUNT(*) AS cnt FROM note_links
|
||||
WHERE target_note_id IS NOT NULL GROUP BY source_note_id
|
||||
) outbound ON outbound.source_note_id = n.id
|
||||
LEFT JOIN (
|
||||
SELECT target_note_id, COUNT(*) AS cnt FROM note_links
|
||||
WHERE target_note_id IS NOT NULL GROUP BY target_note_id
|
||||
) inbound ON inbound.target_note_id = n.id
|
||||
WHERE n.user_id = $1
|
||||
ORDER BY n.updated_at DESC`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer nodeRows.Close()
|
||||
|
||||
for nodeRows.Next() {
|
||||
var node models.NoteGraphNode
|
||||
var tags []string
|
||||
if err := nodeRows.Scan(&node.ID, &node.Title, &node.FolderPath,
|
||||
pq.Array(&tags), &node.UpdatedAt, &node.LinkCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tags == nil {
|
||||
tags = []string{}
|
||||
}
|
||||
node.Tags = tags
|
||||
graph.Nodes = append(graph.Nodes, node)
|
||||
}
|
||||
if err := nodeRows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Resolved edges
|
||||
edgeRows, err := DB.QueryContext(ctx, `
|
||||
SELECT nl.source_note_id, nl.target_note_id, nl.target_title, nl.is_transclusion
|
||||
FROM note_links nl
|
||||
JOIN notes n ON n.id = nl.source_note_id
|
||||
WHERE n.user_id = $1
|
||||
AND nl.target_note_id IS NOT NULL`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer edgeRows.Close()
|
||||
|
||||
for edgeRows.Next() {
|
||||
var edge models.NoteGraphEdge
|
||||
if err := edgeRows.Scan(&edge.Source, &edge.Target,
|
||||
&edge.Title, &edge.IsTransclusion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
graph.Edges = append(graph.Edges, edge)
|
||||
}
|
||||
if err := edgeRows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unresolved links (dangling)
|
||||
danglingRows, err := DB.QueryContext(ctx, `
|
||||
SELECT nl.source_note_id, nl.target_title
|
||||
FROM note_links nl
|
||||
JOIN notes n ON n.id = nl.source_note_id
|
||||
WHERE n.user_id = $1
|
||||
AND nl.target_note_id IS NULL`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer danglingRows.Close()
|
||||
|
||||
for danglingRows.Next() {
|
||||
var d models.NoteGraphDangling
|
||||
if err := danglingRows.Scan(&d.Source, &d.Title); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
graph.Unresolved = append(graph.Unresolved, d)
|
||||
}
|
||||
|
||||
return graph, danglingRows.Err()
|
||||
}
|
||||
@@ -22,6 +22,7 @@ func NewStores(db *sql.DB) store.Stores {
|
||||
Messages: NewMessageStore(),
|
||||
Audit: NewAuditStore(),
|
||||
Notes: NewNoteStore(),
|
||||
NoteLinks: NewNoteLinkStore(),
|
||||
GlobalConfig: NewGlobalConfigStore(),
|
||||
Usage: NewUsageStore(),
|
||||
Pricing: NewPricingStore(),
|
||||
|
||||
Reference in New Issue
Block a user