store

package
v0.14.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 16, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package store owns the SQLite mirror: schema, migrations, transactions, full-text index and the derived fields the source does not provide.

The schema is a public contract documented in specs/000-product/data-model.md — agents query this database directly, so a column change belongs in that document before it belongs here.

Index

Constants

View Source
const (
	FeedWindowDays   = 30
	FeedDefaultLimit = 80
	FeedMaxLimit     = 200
)

Feed window and default page size match the client contract (web/src/lib/api.ts getFeed / FeedFocus).

View Source
const (
	VisitKindIssue = "issue"
	VisitKindPage  = "page"
)

VisitKind is visits.kind / searches.opened_kind: "issue" or "page".

View Source
const (
	CommentsByAuthorDefaultLimit = 50
	CommentsByAuthorMaxLimit     = 200
)

People-axis comment list defaults (GET people/{author_id}/comments/).

View Source
const CategoryDone = "done"

CategoryDone is the only status category with meaning to the derived rules. Every rule keys on the category, never on a status name: the internal tool this was extracted from matched "Reopened" / "다시 열림" and broke on any other site or account language (contracts/sync.md, "Localization hazard").

View Source
const LocalRetention = 180 * 24 * time.Hour

LocalRetention is how long raw visit/search events are kept. Counts older than this window are not rolled up — derive from whatever is still here.

Variables

View Source
var ErrNotFound = errors.New("not found")

ErrNotFound means a single-row lookup (Detail, etc.) found no matching key. Callers map it to 404 / "not in the mirror"; store code still uses sql.ErrNoRows internally and wraps at the package boundary.

Functions

func EnsureLocal added in v0.13.0

func EnsureLocal(mirrorPath string) error

EnsureLocal creates local.db next to the mirror if needed and migrates it. Failures are returned; callers of Open log them and still open the mirror.

func LocalPath added in v0.13.0

func LocalPath(mirrorPath string) string

LocalPath is local.db beside the mirror at mirrorPath.

func Now

func Now() string

Now is the timestamp format every column the store itself writes uses. The server hands the same value to clients as the `delta` cursor, so it must come from here. Milliseconds are not decoration: a whole-second cursor would drop a row written in the same second the cursor was taken.

func OpenReadOnly added in v0.13.0

func OpenReadOnly(path string) (*sql.DB, error)

OpenReadOnly opens the mirror with SQLite mode=ro and ATTACHes local.db (created empty if missing so SELECT local.visits never fails the connection).

Types

type APIUsageDay

type APIUsageDay struct {
	Day             string  `json:"day"`
	Requests        int64   `json:"requests"`
	Throttled       int64   `json:"throttled"`
	ServerErrors    int64   `json:"server_errors"`
	Retries         int64   `json:"retries"`
	WaitMS          int64   `json:"wait_ms"`
	LastThrottledAt *string `json:"last_throttled_at,omitempty"`
}

APIUsageDay is one row of daily accumulated outbound API volume.

type APIUsageDelta

type APIUsageDelta struct {
	Requests        int64
	Throttled       int64
	ServerErrors    int64
	Retries         int64
	WaitMS          int64
	LastThrottledAt string // RFC3339 UTC, empty if unknown this flush
}

APIUsageDelta is a non-negative increment to accumulate into one UTC day. Numbers only — this package must not import jira (layering).

type APIUsageSum

type APIUsageSum struct {
	Requests        int64   `json:"requests"`
	Throttled       int64   `json:"throttled"`
	ServerErrors    int64   `json:"server_errors"`
	Retries         int64   `json:"retries"`
	WaitMS          int64   `json:"wait_ms"`
	LastThrottledAt *string `json:"last_throttled_at,omitempty"`
}

APIUsageSum is a multi-day total (no day key).

type APIUsageSummary

type APIUsageSummary struct {
	Today     APIUsageDay `json:"today"`
	Last7Days APIUsageSum `json:"last_7_days"`
}

APIUsageSummary is the shape status --json and GET settings/ runtime expose: today's row plus a 7-day rollup of our outbound call volume.

type Attachment

type Attachment struct {
	ID         string
	ExternalID string
	Filename   string
	MimeType   string
	Size       int64
	Author     string
	AuthorID   string
	CreatedAt  string
}

Attachment is metadata only. Bytes are proxied on demand, never mirrored.

type AuthorComment

type AuthorComment struct {
	Key       string `json:"key"`
	Kind      string `json:"kind"` // "issue" | "page"
	Title     string `json:"title"`
	Snippet   string `json:"snippet"`
	CreatedAt string `json:"created_at"`
}

AuthorComment is one row in GET people/{author_id}/comments/.

type Batch

type Batch struct {
	Categories map[string]string // status id -> status category
	Priorities []string          // priority display names, most urgent first
	Records    []IssueRecord
	// Force rewrites rows whose updated_at is unchanged. Off, an unchanged row
	// is skipped entirely, which is what keeps an incremental re-run from
	// bumping sync_state.version.
	Force bool
}

Batch is one page of sync output. Categories and Priorities come from the site's own metadata endpoints (which are not localized) and feed the derived field rules.

type ChangeEntry

type ChangeEntry struct {
	ID        string
	At        string
	Author    string
	AuthorID  string
	Field     string // "status", "assignee", "priority", ...
	FromValue string
	FromID    string
	ToValue   string
	ToID      string
}

ChangeEntry is one field change. Status entries carry ids because display names are localized per account.

type Comment

type Comment struct {
	ID         string // "<source_id>:<comment_id>"
	ExternalID string
	Author     string
	AuthorID   string
	BodyADF    json.RawMessage
	BodyText   string
	CreatedAt  string
	UpdatedAt  string
}

Comment is stored flat: the source API exposes no thread parent.

type CommentsByAuthorResult

type CommentsByAuthorResult struct {
	// Author is the display name from the newest matching comment ("" when none).
	Author   string          `json:"author"`
	Total    int             `json:"total"`
	Comments []AuthorComment `json:"comments"`
}

CommentsByAuthorResult is the response body for comments by author.

type DB

type DB struct {
	// contains filtered or unexported fields
}

DB is a handle on the mirror. Safe for concurrent use; writes are serialized.

func Open

func Open(path string) (*DB, error)

Open opens or creates the mirror at path and migrates it forward. A database written by a newer gadak is refused rather than used.

The data directory is created (and existing dirs tightened) to 0700; the DB file and any -wal/-shm sidecars are set to 0600. Chmod failures are logged and ignored so unsupported filesystems (or Windows) still work.

func (*DB) APIUsage

func (db *DB) APIUsage(ctx context.Context, days int) ([]APIUsageDay, error)

APIUsage returns up to the most recent days rows, newest day first. days <= 0 means all rows.

func (*DB) APIUsageSummary

func (db *DB) APIUsageSummary(ctx context.Context) (APIUsageSummary, error)

APIUsageSummary builds today + last 7 days from the mirror. Missing days are zeros. last_throttled_at on the rollup is the latest timestamp in the window.

func (*DB) AddAPIUsage

func (db *DB) AddAPIUsage(ctx context.Context, day string, u APIUsageDelta) error

AddAPIUsage UPSERTs delta into the given UTC day (YYYY-MM-DD), adding to any existing counters. last_throttled_at keeps the latest non-empty timestamp.

func (*DB) AppendSyncRun

func (db *DB) AppendSyncRun(ctx context.Context, sourceID string, r SyncRun) error

AppendSyncRun stores one run and prunes the history to the newest 100.

func (*DB) AttachmentBelongs added in v0.14.1

func (db *DB) AttachmentBelongs(ctx context.Context, issueKey, attachmentID string) (bool, error)

AttachmentBelongs reports whether the mirror lists attachmentID on issueKey. The id is the Jira external id when that column is set, otherwise the store row id — the same rule handleDetail uses when it builds content URLs. One EXISTS query: serving bytes must not pay for Detail's comments/history/links/refs.

func (*DB) Close

func (db *DB) Close() error

func (*DB) CommentsByAuthor

func (db *DB) CommentsByAuthor(ctx context.Context, authorID string, limit int) (CommentsByAuthorResult, error)

CommentsByAuthor returns comments for an exact author_id match, newest first. Missing author_id yields total 0 and an empty list (not an error). limit defaults to 50 and is capped at 200.

func (*DB) ComputeFieldUsage

func (db *DB) ComputeFieldUsage(ctx context.Context, aliases []string) ([]FieldUsageRow, error)

ComputeFieldUsage builds field_usage rows from issues.custom for the given aliases. total is the issue count per project; filled is how many issues have a non-empty value for that alias.

func (*DB) ConfluenceSpaceWatermarks added in v0.13.0

func (db *DB) ConfluenceSpaceWatermarks(ctx context.Context, sourceID string) (map[string]string, error)

ConfluenceSpaceWatermarks returns each space key's incremental floor for sourceID. A missing or NULL watermark is "". Callers treat empty as "not yet backfilled" and run a full fetch for that space.

func (*DB) DeleteItems

func (db *DB) DeleteItems(ctx context.Context, sourceID string, keys []string) (int, error)

DeleteItems removes items whose keys have left the source's scope and records a tombstone so `delta` can report the deletion to a client that missed it.

func (*DB) DeleteSavedView

func (db *DB) DeleteSavedView(ctx context.Context, id string) error

func (*DB) DeletedKeysSince

func (db *DB) DeletedKeysSince(ctx context.Context, since string) ([]string, error)

DeletedKeysSince lists keys tombstoned after the cursor. `delta` must report these: a missed deletion leaves a tombstone visible in the client forever.

func (*DB) Detail

func (db *DB) Detail(ctx context.Context, key string) (*Detail, error)

Detail assembles one issue. An unknown key returns ErrNotFound so the handler can answer 404 without importing database/sql.

func (*DB) DistinctCount

func (db *DB) DistinctCount(ctx context.Context, table, column string) (int, error)

DistinctCount returns COUNT(DISTINCT col) for a fixed known table.column used by doctor. Empty/NULL values are excluded.

func (*DB) EnrichmentsByKind

func (db *DB) EnrichmentsByKind(ctx context.Context, kind string) (map[string]json.RawMessage, error)

EnrichmentsByKind returns every payload of one kind, keyed by issue key. The list and delta responses merge it in.

func (*DB) EnrichmentsFor

func (db *DB) EnrichmentsFor(ctx context.Context, key string) (map[string]json.RawMessage, error)

EnrichmentsFor returns every kind attached to one issue, for the detail view.

func (*DB) Favorites

func (db *DB) Favorites(ctx context.Context) ([]string, error)

func (*DB) Feed

func (db *DB) Feed(ctx context.Context, opts FeedOpts) (FeedResult, error)

Feed computes personal-feed events from the mirror at query time.

func (*DB) FieldUsage

func (db *DB) FieldUsage(ctx context.Context) ([]FieldUsageRow, error)

FieldUsage returns every field_usage row.

func (*DB) FreshenSyncClock

func (db *DB) FreshenSyncClock(ctx context.Context) error

FreshenSyncClock stamps every sync timestamp as now. It exists for throwaway fixtures — `gadak demo` and the demo recordings — where the snapshot's real age would surface as a stale-sync warning about data that is deliberately frozen. Never call this on a mirror that syncs for real: it would hide a stalled sync.

func (*DB) HasCustomFieldKeysInRaw

func (db *DB) HasCustomFieldKeysInRaw(ctx context.Context) (bool, error)

HasCustomFieldKeysInRaw reports whether any stored raw document contains a customfield_ key under fields. Used by `gadak fields --apply` to refuse when the mirror was never synced with custom fields.

func (*DB) HasSource

func (db *DB) HasSource(ctx context.Context, id string) (bool, error)

HasSource reports whether a sources row exists for id.

func (*DB) History added in v0.13.0

func (db *DB) History(ctx context.Context, opts HistoryOpts) (HistoryPage, error)

History returns visits and searches newest-first. Cursor is opaque.

func (*DB) IssueLites

func (db *DB) IssueLites(ctx context.Context) ([]IssueLite, error)

IssueLites returns the whole mirror, which is what `bootstrap` sends.

func (*DB) IssueLitesByKeys added in v0.14.1

func (db *DB) IssueLitesByKeys(ctx context.Context, keys []string) ([]IssueLite, error)

IssueLitesByKeys returns the IssueLite rows for keys, in the order asked. Missing and empty keys are skipped. Duplicate keys yield duplicate rows. The SQL IN-list is de-duplicated; callers that need FTS rank pass the ranked key list and get that order back.

func (*DB) IssueLitesSince

func (db *DB) IssueLitesSince(ctx context.Context, since string) ([]IssueLite, error)

IssueLitesSince returns rows written at or after the given cursor. Only a row whose content actually changed has a newer synced_at, so an idle poll returns none. The bound is inclusive on purpose: re-sending a row the client already has is a harmless upsert, while missing one leaves it stale forever.

func (*DB) MarkFeedRead

func (db *DB) MarkFeedRead(ctx context.Context, opts MarkFeedReadOpts) (MarkFeedReadResult, error)

MarkFeedRead upserts read receipts and returns the refreshed unread counts.

func (*DB) PageDetail

func (db *DB) PageDetail(ctx context.Context, key string) (*PageDetail, error)

PageDetail assembles one page. An unknown key returns (nil, nil).

func (*DB) PageLites

func (db *DB) PageLites(ctx context.Context) ([]PageLite, error)

PageLites returns every mirrored page, ordered by space then title.

func (*DB) PageStamps added in v0.14.2

func (db *DB) PageStamps(ctx context.Context, sourceID, spaceKey string) (map[string]PageStamp, error)

PageStamps returns the version stamp of every mirrored page in one space, keyed by the source's own page id (items.external_id). It exists so an incremental sync can answer "do I already hold this page?" from the mirror instead of spending a body fetch to find out — a search hit already carries version.number and version.when.

The mirror is a disposable cache, so a missing or malformed row simply means "fetch it": callers must treat an absent key as unknown, never as unchanged.

func (*DB) PruneConfluenceSpaces added in v0.13.0

func (db *DB) PruneConfluenceSpaces(ctx context.Context, sourceID string, keepKeys []string) (int, error)

PruneConfluenceSpaces deletes mirrored wiki pages (and their items, FTS rows, and cascaded children) whose space_key is not in keepKeys, then drops the matching spaces rows. keepKeys empty is a no-op so a zero-scope call cannot wipe the mirror. The returned count is the number of pages removed (space-row-only cleanup is not counted).

func (*DB) PruneLocalHistory added in v0.13.0

func (db *DB) PruneLocalHistory(ctx context.Context) error

PruneLocalHistory deletes visit/search rows older than LocalRetention. Called from the same pass as tombstone expiry (DeleteItems).

func (*DB) PutSavedView

func (db *DB) PutSavedView(ctx context.Context, v SavedView) error

PutSavedView inserts or replaces a view. The caller owns id generation.

func (*DB) RecordSearch added in v0.13.0

func (db *DB) RecordSearch(ctx context.Context, query string, resultCount int, openedKind, openedKey string) (Search, error)

RecordSearch appends one search. openedKind/openedKey may both be empty, or both set to the item opened from this search.

func (*DB) RecordSync

func (db *DB) RecordSync(ctx context.Context, sourceID string, r SyncResult) error

RecordSync stores the run's bookkeeping. It does not bump `version`: a run that changed no rows must leave the ETag alone. A successful run advances first_sync_at (once) and sync_count.

func (*DB) RecordVisit added in v0.13.0

func (db *DB) RecordVisit(ctx context.Context, kind, key string) (Visit, error)

RecordVisit appends one view. Same (kind, key) twice is two rows.

func (*DB) ReingestCustom

func (db *DB) ReingestCustom(ctx context.Context, specs []fields.SpecIDs, bodyFieldIDs []string) (int, error)

ReingestCustom recomputes issues.custom and the FTS body from the stored raw JSON — no network. Returns the number of issues rewritten.

func (*DB) ReplaceFieldUsage

func (db *DB) ReplaceFieldUsage(ctx context.Context, rows []FieldUsageRow) error

ReplaceFieldUsage replaces the entire field_usage table.

func (*DB) ReplaceSourceQueries added in v0.13.0

func (db *DB) ReplaceSourceQueries(ctx context.Context, sourceID string, queries []SourceQuery) error

ReplaceSourceQueries swaps one source's named queries in a single write. An empty list clears that source (the account deleted every filter).

func (*DB) SavedViews

func (db *DB) SavedViews(ctx context.Context) ([]SavedView, error)

func (*DB) ScanFieldFill

func (db *DB) ScanFieldFill(ctx context.Context, fn func(projectKey string, fieldVals map[string]json.RawMessage) error) error

ScanFieldFill streams every issue's project key and the fields object from stored raw JSON. Rows with empty or unparseable raw are skipped (older rows). fn receives field id → raw value for custom and system fields present in raw.

func (*DB) SchemaVersion

func (db *DB) SchemaVersion() int

SchemaVersion is the migration level this binary applied.

func (*DB) Search

func (db *DB) Search(ctx context.Context, query string, limit int) (SearchResult, error)

Search runs an FTS5 query over titles, bodies and comment text and returns matching issues and pages, best match first. Bare terms are rewritten as quoted prefix queries (see ftsPrefixQuery). A query FTS5 cannot parse is retried as a literal phrase rather than surfaced as an error, because this is fed raw user input. limit applies to the combined FTS result set.

func (*DB) SetFavorite

func (db *DB) SetFavorite(ctx context.Context, key string, on bool) error

SetFavorite adds or removes a favorite issue key.

func (*DB) SetLastNotifiedAt

func (db *DB) SetLastNotifiedAt(ctx context.Context, sourceID, at string) error

SetLastNotifiedAt advances the OS-notification watermark. Never touches feed_reads.

func (*DB) SetSearchOpened added in v0.13.0

func (db *DB) SetSearchOpened(ctx context.Context, id int64, kind, key string) (Search, error)

SetSearchOpened stamps the item opened from an existing search row.

func (*DB) SetSpaceWatermark added in v0.13.0

func (db *DB) SetSpaceWatermark(ctx context.Context, sourceID, key, watermark string) error

SetSpaceWatermark writes one space's incremental floor. An unknown key is inserted so a just-scoped space can be stamped after its first successful pass without a separate UpsertSpaces.

func (*DB) SetWatch

func (db *DB) SetWatch(ctx context.Context, key string, on bool) error

SetWatch adds or removes a watched issue key.

func (*DB) SourceQueries added in v0.13.0

func (db *DB) SourceQueries(ctx context.Context, sourceID string) ([]SourceQuery, error)

SourceQueries returns one source's named queries, starred first then by name.

func (*DB) SyncRuns

func (db *DB) SyncRuns(ctx context.Context, sourceID string, limit int) ([]SyncRun, error)

SyncRuns returns the newest runs first, at most limit.

func (*DB) SyncState

func (db *DB) SyncState(ctx context.Context, sourceID string) (SyncState, error)

SyncState reads the state for one source. A source that has never synced returns a zero state, not an error.

func (*DB) TableCount

func (db *DB) TableCount(ctx context.Context, table string) (int, error)

TableCount returns SELECT COUNT(*) for a fixed known table name used by status and doctor. Only whitelisted table names are accepted.

func (*DB) UpsertIssues

func (db *DB) UpsertIssues(ctx context.Context, b Batch) (int, error)

UpsertIssues writes one page of sync output in a single transaction and returns how many items it actually changed. Derived fields are recomputed from the batch's changelog, never carried over (contracts/sync.md invariant 5).

An item whose updated_at is unchanged is skipped whole — children, FTS and the version counter included — unless Batch.Force is set. That is what makes an incremental re-run over the watermark overlap window a no-op.

func (*DB) UpsertPages

func (db *DB) UpsertPages(ctx context.Context, records []PageRecord) (int, error)

UpsertPages writes document records in a single transaction and returns how many items it actually changed. A page whose stored body, meta and comments match the incoming record is skipped (no version bump). Comment-only edits still bump because comments are part of the compare — that is the comments- only trap the old always-rewrite path existed to close.

func (*DB) UpsertSource

func (db *DB) UpsertSource(ctx context.Context, s Source) error

UpsertSource records a connector instance. Credentials never come near it (Constitution Article 8).

func (*DB) UpsertSpaces

func (db *DB) UpsertSpaces(ctx context.Context, sourceID string, rows []SpaceRow) error

UpsertSpaces writes wiki space rows (key → name/kind/homepage_id) for a source. Empty name/kind/homepage_id on conflict keeps the previous value so page-hit upserts (name only) do not wipe kind or homepage filled by a full space listing or per-space GET.

func (*DB) Watches

func (db *DB) Watches(ctx context.Context) ([]string, error)

type DeriveInput

type DeriveInput struct {
	Changelog       []ChangeEntry
	Categories      map[string]string // status id -> new | inprogress | done
	CurrentCategory string            // the issue's category right now
	Priority        string
	Priorities      []string // site priority names, most urgent first
	Comments        []Comment
	Links           []Link
}

DeriveInput is everything the derived-field rules need. Changelog entries carry only status ids, so the id -> category map has to come from the site's status list, which the connector supplies per batch.

type Derived

type Derived struct {
	StatusChangedAt   *string
	ResolvedAt        *string
	ReopenCount       int
	ReopenedAt        *string
	ReopenReason      string
	AssigneeChangedAt *string
	CommentCount      int
	PriorityRank      int
	ClonedFrom        string
}

Derived holds the columns gadak computes because the source does not provide them. Rules are documented in data-model.md, "Derived field rules".

func Derive

func Derive(in DeriveInput) Derived

Derive computes every derived field in one pass over the issue's changelog. A status id missing from the category map counts as not-done, which can only ever miss a reopen — never invent one.

type Detail

type Detail struct {
	IssueKey       string             `json:"issue_key"`
	DescriptionADF json.RawMessage    `json:"description_adf"`
	Comments       []DetailComment    `json:"comments"`
	Attachments    []DetailAttachment `json:"attachments"`
	History        []DetailChange     `json:"history"`
	LinkedIssues   []DetailLink       `json:"linked_issues"`
	// RefPages are wiki pages this issue's body/comments mention (item_refs,
	// target_kind=page). Only pages present in the mirror; empty omitted.
	RefPages []PageLite `json:"ref_pages,omitempty"`
	// BacklinkPages are wiki pages that mention this issue key. Empty omitted.
	BacklinkPages []PageLite `json:"backlink_pages,omitempty"`
	// Custom is the issue's full alias→value map. List rows strip body-role
	// values (they can be document-sized); detail is where they surface.
	Custom map[string]any `json:"-"`
}

Detail is everything the on-demand detail view needs, assembled from the mirror with no call to the source.

type DetailAttachment

type DetailAttachment struct {
	ID         string `json:"id"`
	ExternalID string `json:"external_id"`
	Filename   string `json:"filename"`
	MimeType   string `json:"mime_type"`
	Size       int64  `json:"size"`
	Author     string `json:"author,omitempty"`
	AuthorID   string `json:"author_id,omitempty"`
	CreatedAt  string `json:"created_at"`
}

DetailAttachment is metadata only; the handler turns ExternalID into the content proxy path.

type DetailChange

type DetailChange struct {
	At        string `json:"at"`
	Author    string `json:"author"`
	AuthorID  string `json:"author_id,omitempty"`
	Field     string `json:"field"`
	FromValue string `json:"from_value"`
	FromID    string `json:"from_id"`
	ToValue   string `json:"to_value"`
	ToID      string `json:"to_id"`
}

DetailChange is one history row. The ids come along because display values are localized: a caller that wants the `from_category` / `to_category` the detail contract allows has to resolve them from an id, never from a name.

type DetailComment

type DetailComment struct {
	ID         string          `json:"id"`
	ExternalID string          `json:"external_id"`
	Author     string          `json:"author"`
	AuthorID   string          `json:"author_id"`
	BodyADF    json.RawMessage `json:"body_adf"`
	Body       string          `json:"body"` // flattened; the client's fallback when ADF will not render
	CreatedAt  string          `json:"created_at"`
	UpdatedAt  string          `json:"updated_at"`
}

DetailComment is one comment as the detail panel renders it.

type DetailLink struct {
	Key            string `json:"key"`
	Type           string `json:"type"`
	Direction      string `json:"direction"`
	Summary        string `json:"summary"`
	StatusCategory string `json:"status_category"`
}

DetailLink is an edge plus whatever the mirror knows about the far side. A target outside the mirror keeps an empty summary and category.

type FeedFocus

type FeedFocus string

FeedFocus is the focus tab the client sends: all | assignee | reporter | mention. "watched" is a reason, not a focus filter.

const (
	FeedFocusAll      FeedFocus = "all"
	FeedFocusAssignee FeedFocus = "assignee"
	FeedFocusReporter FeedFocus = "reporter"
	FeedFocusMention  FeedFocus = "mention"
)

type FeedIdentity

type FeedIdentity struct {
	AccountID   string
	Email       string
	DisplayName string // TokenOwner / display name for author matching
}

FeedIdentity is the local user, used for relevance and self-action exclusion. AccountID comes from config (Jira /myself); Email/DisplayName fall back when the mirror still only has display names on some rows.

type FeedItem

type FeedItem struct {
	ID            int            `json:"id"`
	EventID       string         `json:"event_id"`
	IssueKey      string         `json:"issue_key"`
	Summary       string         `json:"summary"`
	CurrentStatus string         `json:"current_status"`
	EventType     string         `json:"event_type"`
	OccurredAt    *string        `json:"occurred_at"`
	ActorName     string         `json:"actor_name"`
	Payload       map[string]any `json:"payload"`
	Reasons       []string       `json:"reasons"`
	ReadAt        *string        `json:"read_at"`
}

FeedItem is one activity row the client renders (web/src/lib/types.ts FeedItem).

type FeedOpts

type FeedOpts struct {
	Focus FeedFocus
	Limit int
	Me    FeedIdentity
	// Now, when set, freezes the 30-day window for tests.
	Now time.Time
}

FeedOpts configures a personal-feed query.

type FeedResult

type FeedResult struct {
	Items        []FeedItem       `json:"items"`
	UnreadCounts FeedUnreadCounts `json:"unread_counts"`
}

FeedResult is GET feed/ response body (without the outer wrapper keys).

type FeedUnreadCounts

type FeedUnreadCounts struct {
	All      int `json:"all"`
	Assignee int `json:"assignee"`
	Reporter int `json:"reporter"`
	Mention  int `json:"mention"`
}

FeedUnreadCounts is the badge counts per focus tab.

type FieldUsageRow

type FieldUsageRow struct {
	ProjectKey string
	Alias      string
	Filled     int
	Total      int
}

FieldUsageRow is one (project, alias) fill statistic from field_usage.

type HistoryItem added in v0.13.0

type HistoryItem struct {
	Type        string  `json:"type"` // visit | search
	ID          int64   `json:"id"`
	Kind        string  `json:"kind,omitempty"`
	Key         string  `json:"key,omitempty"`
	Query       string  `json:"query,omitempty"`
	ResultCount *int    `json:"result_count,omitempty"`
	OpenedKind  *string `json:"opened_kind,omitempty"`
	OpenedKey   *string `json:"opened_key,omitempty"`
	At          string  `json:"at"`
}

HistoryItem is one row of the mixed timeline (newest first).

type HistoryOpts added in v0.13.0

type HistoryOpts struct {
	// Kind is "issue", "page", or "search". Empty means all three.
	Kind   string
	Limit  int
	Cursor string
}

HistoryOpts filters the mixed visit+search timeline.

type HistoryPage added in v0.13.0

type HistoryPage struct {
	Items      []HistoryItem `json:"items"`
	NextCursor string        `json:"next_cursor,omitempty"`
}

HistoryPage is one cursor page of History.

type Issue

type Issue struct {
	ProjectKey     string
	IssueType      string
	IssueTypeID    string
	Status         string
	StatusID       string
	StatusCategory string // new | inprogress | done
	Priority       string
	Assignee       string
	AssigneeID     string
	AssigneeEmail  string
	Reporter       string
	ReporterID     string
	ReporterEmail  string
	ParentKey      string
	// HierarchyLevel is the source-neutral tree rank of this issue's type
	// (e.g. epic=1, standard=0, sub-task=-1). Used to derive EpicKey.
	HierarchyLevel  int
	Labels          []string
	Components      []string
	FixVersions     []string
	AffectsVersions []string
	EnvironmentText string
	Duedate         string
	Resolution      string
	DescriptionADF  json.RawMessage
	Custom          map[string]any // mapped custom fields, keyed by config alias
	Raw             json.RawMessage
}

Issue is the tracker projection. `item_id`, `key`, `created_at`, `updated_at` are taken from the record's Item and every derived field is computed by the store, so a connector fills none of them.

type IssueLite

type IssueLite struct {
	IssueKey       string  `json:"issue_key"`
	Summary        string  `json:"summary"`
	ProjectKey     string  `json:"project_key"`
	IssueType      string  `json:"issue_type"`
	IssueTypeID    string  `json:"issue_type_id"`
	Status         string  `json:"status"`
	StatusID       string  `json:"status_id"`
	StatusCategory string  `json:"status_category"`
	Priority       *string `json:"priority"`
	// PriorityID is the stable Jira priority id. Empty until a later schema
	// column is filled — the field is on the wire so clients can match id-first
	// the same way they do for status_id / issue_type_id. There is no
	// issues.priority_id column today (only name + rank).
	PriorityID    string  `json:"priority_id"`
	PriorityRank  int     `json:"priority_rank"`
	Assignee      *string `json:"assignee"`
	AssigneeID    *string `json:"assignee_id"`
	AssigneeEmail *string `json:"assignee_email"`
	Reporter      *string `json:"reporter"`
	ReporterID    *string `json:"reporter_id"`
	ReporterEmail *string `json:"reporter_email"`
	// EpicKey is the nearest hierarchy_level==1 ancestor (derived). Nil when
	// none. Distinct from ParentKey, which is the direct parent only.
	EpicKey *string `json:"epic_key"`
	// ParentKey is the direct parent issue key (source field), not the epic.
	ParentKey       *string  `json:"parent_key"`
	Labels          []string `json:"labels"`
	Components      []string `json:"components"`
	FixVersions     []string `json:"fix_versions"`
	Duedate         *string  `json:"duedate"`
	Resolution      *string  `json:"resolution"`
	CreatedAt       *string  `json:"created_at"`
	UpdatedAt       *string  `json:"updated_at"`
	StatusChangedAt *string  `json:"status_changed_at"`
	ResolvedAt      *string  `json:"resolved_at"`
	ReopenCount     int      `json:"reopen_count"`
	ReopenedAt      *string  `json:"reopened_at"`
	ReopenReason    *string  `json:"reopen_reason"`
	ClonedFrom      *string  `json:"cloned_from"`
	// SourceProject is ClonedFrom's project prefix, precomputed because the
	// list filter groups by it.
	SourceProject *string `json:"source_project"`
	CommentCount  int     `json:"comment_count"`
	// Custom holds the configured field aliases from issues.custom. The server
	// spreads them into the response as top-level keys, which is where the client
	// reads severity and friends.
	Custom map[string]any `json:"custom,omitempty"`
}

IssueLite is the row shape the read API hydrates the client with. Field names are the ones web/src/lib/types.ts already parses (contracts/api.md, "IssueLite"): adding is safe, renaming is not.

type IssueRecord

type IssueRecord struct {
	Item        Item
	Issue       Issue
	Comments    []Comment
	Attachments []Attachment
	Changelog   []ChangeEntry
	Links       []Link
}

IssueRecord is one item and everything hanging off it. Child lists are replaced wholesale on upsert, so a partial list would delete rows.

type Item

type Item struct {
	ID         string // "<source_id>:<external_id>"
	SourceID   string
	Kind       string // "issue" in v0.1
	ExternalID string
	Key        string // human-facing key, unique per source
	Title      string
	BodyText   string // flattened body, what FTS indexes
	Author     string
	AuthorID   string
	URL        string
	CreatedAt  string
	UpdatedAt  string
}

Item is the neutral spine row: anything with a title, body, author and timestamp fits it.

type ItemRef

type ItemRef struct {
	TargetKind string // "issue" | "page"
	TargetKey  string // issue key or page id (numeric string)
	Via        string // "url" | "text"
}

ItemRef is one outgoing reference from an item. Pure extraction returns these; the write path persists them into item_refs.

func ExtractIssueRefsFromPage

func ExtractIssueRefsFromPage(bodyADF, bodyText string, knownProjects map[string]bool) []ItemRef

ExtractIssueRefsFromPage finds issue keys in a page's ADF (URL paths) and plain body_text (bare keys filtered by knownProjects). Same key from both sources yields one row with via=url. knownProjects may be empty (no text hits).

func ExtractPageRefsFromIssue

func ExtractPageRefsFromIssue(bodyADF, bodyText string, commentBodies []string) []ItemRef

ExtractPageRefsFromIssue finds Confluence page IDs in an issue description (raw ADF + flattened text) and its comment bodies. Scanning ADF is what picks up link marks and inlineCards that PlainText drops — the same raw scan ExtractIssueRefsFromPage already does in the other direction. Both URL shapes use via=url. Duplicates collapse to one.

type Link struct {
	Type      string
	Direction string // inward | outward
	TargetKey string
}

Link is an edge out of an item. TargetKey may point outside the mirror.

type MarkFeedReadOpts

type MarkFeedReadOpts struct {
	EventIDs  []string
	IssueKeys []string
	All       bool
	Me        FeedIdentity
	Now       time.Time
}

MarkFeedReadOpts is POST feed/read/ body semantics.

type MarkFeedReadResult

type MarkFeedReadResult struct {
	Updated      int              `json:"updated"`
	UnreadCounts FeedUnreadCounts `json:"unread_counts"`
}

MarkFeedReadResult is POST feed/read/ response.

type Page

type Page struct {
	SpaceKey string
	ParentID string
	Version  int
	Status   string
	// Labels are source-neutral tag names (JSON array column). Empty slice is
	// stored as "[]", never NULL. Sync sorts alphabetically for determinism.
	Labels []string
	// BodyADF is the raw Atlas Document Format body for rendering. Empty when
	// the mirror only has flattened body_text (pre-v10 rows).
	BodyADF json.RawMessage
}

Page is the document projection (one row in the pages table). Field names match the projection columns, not any source API (same status as Issue).

type PageComment

type PageComment struct {
	Author    string          `json:"author"`
	CreatedAt string          `json:"created_at"`
	BodyADF   json.RawMessage `json:"body_adf"`
	BodyText  string          `json:"body_text"`
}

PageComment is one comment on a page detail response.

type PageDetail

type PageDetail struct {
	PageLite
	BodyADF  json.RawMessage `json:"body_adf"`
	Comments []PageComment   `json:"comments"`
	// RefIssueKeys are issue keys this page's body mentions (item_refs). Only
	// issues present in the mirror, sorted ascending. Empty omitted.
	RefIssueKeys []string `json:"ref_issue_keys,omitempty"`
	// BacklinkIssueKeys are issue keys that mention this page. Empty omitted.
	BacklinkIssueKeys []string `json:"backlink_issue_keys,omitempty"`
}

PageDetail is PageLite plus the raw ADF body and comments.

type PageLite

type PageLite struct {
	Key      string `json:"key"`
	Title    string `json:"title"`
	SpaceKey string `json:"space_key"`
	// SpaceName is the human-readable space title from the spaces table join.
	// Empty when no spaces row is mirrored for this key (never omitted in JSON).
	SpaceName string `json:"space_name"`
	// SpaceHomepageID is the content id of the space root page (spaces.homepage_id).
	// Empty when the space row is missing or homepage has not been learned yet.
	SpaceHomepageID string `json:"space_homepage_id"`
	ParentID        string `json:"parent_id"`
	Author          string `json:"author"`
	// AuthorID is items.author_id. Empty on legacy rows that only have a
	// display name — clients group by this when present and fall back to Author.
	AuthorID  string `json:"author_id"`
	UpdatedAt string `json:"updated_at"`
	Version   int    `json:"version"`
	URL       string `json:"url"`
	// Excerpt is a one-line body preview derived from body_adf (schema v15):
	// whitespace-normalized, at most 200 runes. Empty when the body is empty.
	Excerpt string   `json:"excerpt"`
	Labels  []string `json:"labels"`
}

PageLite is the list/search row for a mirrored wiki page. Field names are snake_case JSON to match IssueLite and the read API contract.

type PageRecord

type PageRecord struct {
	Item     Item
	Page     Page
	Comments []Comment
}

PageRecord is one document item plus its projection and comments. Comments are replaced wholesale on upsert, matching IssueRecord.

type PageStamp added in v0.14.2

type PageStamp struct {
	Version   int
	UpdatedAt string
}

PageStamp is the mirror's record of one page's upstream identity: the source's version number and the lastModified the row was written from. Both together, never the number alone — a rollback can reuse a number.

type SavedView

type SavedView struct {
	ID        string          `json:"id"`
	Name      string          `json:"name"`
	Config    json.RawMessage `json:"config"`
	CreatedAt string          `json:"created_at"`
	UpdatedAt string          `json:"updated_at"`
}

SavedView is a user's stored filter set. Personal state is the only thing in this database a user would miss, so it is also the only thing `gadak export` has to dump (Constitution Article 1).

type Search struct {
	ID          int64   `json:"id"`
	Query       string  `json:"query"`
	SearchedAt  string  `json:"searched_at"`
	ResultCount int     `json:"result_count"`
	OpenedKind  *string `json:"opened_kind"`
	OpenedKey   *string `json:"opened_key"`
}

Search is one append-only search execution. OpenedKind/OpenedKey name the item opened from that search, when one was.

type SearchMatch

type SearchMatch struct {
	Field   string `json:"field"`
	Snippet string `json:"snippet"`
}

SearchMatch says which FTS column matched and shows a plain-text snippet. Field is "title" | "body" | "comment". Snippet has no HTML or highlight markers — the client highlights against its own query string.

type SearchResult

type SearchResult struct {
	Keys    []string               `json:"keys"`
	Pages   []PageLite             `json:"pages"`
	Total   int                    `json:"total"`
	Matches map[string]SearchMatch `json:"matches"`
}

SearchResult is a kind-aware FTS hit list. Keys are issue keys (best match first among issues in the ranked window); Pages are page hits in the same ranked window. Total is the number of hits returned (keys + pages), matching the pre-R2 meaning of total = len(results after limit). Matches maps each returned issue or page key to the winning column match.

type Source

type Source struct {
	ID      string // stable slug, e.g. "jira"
	Kind    string
	BaseURL string
}

Source is one configured connector instance.

type SourceQuery added in v0.13.0

type SourceQuery struct {
	ID          string          `json:"id"`
	SourceID    string          `json:"source_id"`
	ExternalID  string          `json:"external_id"`
	Name        string          `json:"name"`
	QueryText   string          `json:"query_text"`
	Config      json.RawMessage `json:"config"`
	Favourite   bool            `json:"favourite"`
	Owner       string          `json:"owner"`
	Applied     []string        `json:"applied"`
	Unsupported []string        `json:"unsupported"`
	UpdatedAt   string          `json:"updated_at"`
}

SourceQuery is a named query mirrored from a connector (Jira saved filter). Jira is the record: ReplaceSourceQueries rewrites one source's rows.

type SpaceRow

type SpaceRow struct {
	Key        string
	Name       string
	Kind       string
	HomepageID string
}

SpaceRow is one wiki space (key + human name + kind). Source-neutral: a connector maps its space listing or page-embedded space ref onto this. Kind is the source type string (e.g. "global", "personal"); empty is allowed when only a name is known from a page hit. HomepageID is the content id of the space root page when known; empty on page-hit upserts so they do not wipe a value filled by a space listing.

type SyncResult

type SyncResult struct {
	Watermark string // ignored when empty or not greater than the stored one
	FullSync  bool   // stamps last_full_sync_at
	Err       error  // recorded as last_error; nil clears it
}

SyncResult is what a finished sync run reports.

type SyncRun

type SyncRun struct {
	Kind       string `json:"kind"` // full | incremental (+reconcile)
	StartedAt  string `json:"started_at"`
	FinishedAt string `json:"finished_at"`
	Fetched    int    `json:"fetched"`
	Changed    int    `json:"changed"`
	Deleted    int    `json:"deleted"`
	Error      string `json:"error,omitempty"`
}

SyncRun is one recorded sync pass. Only meaningful runs are stored: ones that changed something, were a full pass, or failed — the watch loop's no-op incrementals would otherwise bury the history in noise.

type SyncState

type SyncState struct {
	SourceID  string `json:"source_id"`
	Watermark string `json:"watermark"`
	// SyncedAt is the last run that finished without an error — the only field
	// here that means "the mirror is fresh". A watermark stalls on its own
	// whenever the project is simply quiet.
	SyncedAt       *string `json:"synced_at"`
	Version        int64   `json:"version"`
	LastFullSyncAt *string `json:"last_full_sync_at"`
	LastError      *string `json:"last_error"`
	SchemaVersion  int     `json:"schema_version"`
	// FirstSyncAt is the first successful sync for this source (retention).
	FirstSyncAt *string `json:"first_sync_at,omitempty"`
	// SyncCount is the number of successful sync runs (retention).
	SyncCount int64 `json:"sync_count"`
	// LastNotifiedAt is the OS-notification watermark. Independent of
	// feed_reads: delivering a desktop alert must not mark the feed read.
	LastNotifiedAt *string `json:"last_notified_at,omitempty"`
}

SyncState is the per-source sync bookkeeping.

type Visit added in v0.13.0

type Visit struct {
	ID       int64  `json:"id"`
	Kind     string `json:"kind"`
	Key      string `json:"key"`
	ViewedAt string `json:"viewed_at"`
}

Visit is one append-only view event.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL