repository

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 8, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NormalizePair

func NormalizePair(a, b uuid.UUID) (uuid.UUID, uuid.UUID)

NormalizePair returns (lo, hi) with lo.String() <= hi.String(), so cleanup_queue always stores the smaller UUID in doc_a_id — preventing two rows per unordered pair.

func ReadTenants

func ReadTenants(tenantID uuid.UUID) []uuid.UUID

ReadTenants is the exported form of readTenants for service-layer WRITE-path callers of the set-based read methods (GetByPath/GetByID/List): it preserves the single-tenant + common-pool scope so writes and the guest-editor common-pool path keep their exact pre-aggregation behavior. READ callers pass the service-computed readable set instead.

Types

type APIKeyRepository

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

func NewAPIKeyRepository

func NewAPIKeyRepository(db *gorm.DB) *APIKeyRepository

func (*APIKeyRepository) Create

func (r *APIKeyRepository) Create(ctx context.Context, key *models.APIKey) error

func (*APIKeyRepository) Delete

func (r *APIKeyRepository) Delete(ctx context.Context, id uuid.UUID) error

Delete permanently removes an API key row. Unlike Revoke (which sets revoked_at and preserves the row/audit trail for normal key retirement), this hard-deletes — APIKey has no soft-delete column, so this is a real DELETE. Reserved for the break-glass reset, which must remove the admin key entirely (re-arming bootstrap), not merely disable it.

func (*APIKeyRepository) FindBySubjectID

func (r *APIKeyRepository) FindBySubjectID(ctx context.Context, subjectID string) ([]models.APIKey, error)

FindBySubjectID returns every API key whose authz subject (authzseed.APIKeySubjectID: explicit subject_id, else the tenant service principal "svc:<tenant_id>") equals subjectID. Mirrors that resolution in SQL so callers can reverse-map an authz subject back to the key row(s) that mint it — used by the break-glass reset to find the admin key(s) behind a system:memory#admin tuple's subject.

func (*APIKeyRepository) GetByID

func (r *APIKeyRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.APIKey, error)

GetByID returns a single key by id (including revoked/expired rows — callers like rotation need the metadata regardless of current validity).

func (*APIKeyRepository) ListByTenant

func (r *APIKeyRepository) ListByTenant(ctx context.Context, tenantID uuid.UUID) ([]models.APIKey, error)

func (*APIKeyRepository) PurgeDeadBefore

func (r *APIKeyRepository) PurgeDeadBefore(ctx context.Context, cutoff time.Time) (int64, error)

PurgeDeadBefore hard-deletes keys that went dead — revoked or expired — strictly before cutoff, so long-retired keys stop cluttering listings. Returns the number of rows removed. Used by the scheduled dead-key sweep.

func (*APIKeyRepository) Revoke

func (r *APIKeyRepository) Revoke(ctx context.Context, id uuid.UUID) error

Revoke sets revoked_at on the key, effectively disabling it.

func (*APIKeyRepository) SetExpiry

func (r *APIKeyRepository) SetExpiry(ctx context.Context, id uuid.UUID, t *time.Time) error

SetExpiry sets expires_at on a key (used by rotation's grace window to let a predecessor stay valid until it lapses). A nil t clears any expiry.

type CleanupQueueRepository

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

func NewCleanupQueueRepository

func NewCleanupQueueRepository(db *gorm.DB) *CleanupQueueRepository

func (*CleanupQueueRepository) CountPending

func (r *CleanupQueueRepository) CountPending(ctx context.Context, tenantID uuid.UUID) (int64, error)

CountPending returns the number of unresolved entries for the tenant.

func (*CleanupQueueRepository) GetByID

func (r *CleanupQueueRepository) GetByID(ctx context.Context, tenantID uuid.UUID, id uuid.UUID) (*models.CleanupQueue, error)

GetByID returns a queue entry by id, scoped to the caller's tenants (own + common pool). ErrNotFound when out of scope. Used to resolve the entry's referenced document before an authorization Check.

func (*CleanupQueueRepository) ListAll

func (r *CleanupQueueRepository) ListAll(ctx context.Context, tenantID uuid.UUID, limit int) ([]models.CleanupQueue, error)

ListAll returns queue entries for the tenant, most recent first.

func (*CleanupQueueRepository) ListPending

func (r *CleanupQueueRepository) ListPending(ctx context.Context, tenantID uuid.UUID, limit int) ([]models.CleanupQueue, error)

ListPending returns unresolved queue entries, newest first.

func (*CleanupQueueRepository) Resolve

func (r *CleanupQueueRepository) Resolve(ctx context.Context, tenantID uuid.UUID, id uuid.UUID, resolution string, note string, mergedInto *uuid.UUID) error

Resolve marks a queue entry as resolved with the given resolution/note. If resolution is "merged", set mergedInto to the surviving doc ID.

func (*CleanupQueueRepository) Upsert

Upsert inserts a queue entry for the (tenant, doc_a, doc_b) pair only if no unresolved row exists for the same unordered pair. Returns true if inserted.

type DocumentRepository

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

func NewDocumentRepository

func NewDocumentRepository(db *gorm.DB) *DocumentRepository

func (*DocumentRepository) Create

func (r *DocumentRepository) Create(ctx context.Context, doc *models.Document) error

func (*DocumentRepository) Delete

func (r *DocumentRepository) Delete(ctx context.Context, tenantID uuid.UUID, id uuid.UUID) error

func (*DocumentRepository) GenerateIndex

func (r *DocumentRepository) GenerateIndex(ctx context.Context, tenantIDs []uuid.UUID, depth IndexDepth, category *string) ([]IndexEntry, error)

GenerateIndex produces a tiered catalog of documents over a tenant-id set (the caller's readable scope), filtering tenant_id IN (?).

  • summary: one row per (tenant, category, subcategory) with COUNT and aggregated titles
  • category: one row per document (DocCount=1, Topics=title), filtered by category
  • full: one row per document (DocCount=1, Topics=title), all categories

func (*DocumentRepository) GetByID

func (r *DocumentRepository) GetByID(ctx context.Context, tenantIDs []uuid.UUID, id uuid.UUID) (*models.Document, error)

GetByID resolves a document by primary key, scoped to the given tenant-id set.

func (*DocumentRepository) GetByPath

func (r *DocumentRepository) GetByPath(ctx context.Context, tenantIDs []uuid.UUID, home uuid.UUID, category string, subcategory *string, slug string) (*models.Document, error)

GetByPath resolves a document by path across the given tenant-id set. `home` is the requesting tenant used only for ordering: a doc owned by `home` is preferred over one in the common pool or another readable tenant when the same path exists in several.

func (*DocumentRepository) List

func (r *DocumentRepository) List(ctx context.Context, tenantIDs []uuid.UUID, category *string, subcategory *string, limit, offset int) ([]models.Document, error)

List returns documents across the given tenant-id set, optionally filtered. A positive limit paginates via LIMIT/OFFSET; limit <= 0 returns the full list. The order carries an id tiebreak because (category, subcategory, slug) is not unique across the aggregated tenant set, so offset paging is total — no page skips or duplicates a row (design D6).

func (*DocumentRepository) Save

func (r *DocumentRepository) Save(ctx context.Context, tenantID uuid.UUID, doc *models.Document) error

Save persists doc (and, via GORM associations, its sections) scoped to tenantID. gorm's db.Save is a PK-keyed UPDATE with no tenant_id predicate, so before saving we verify the row actually exists under tenantID: a cross-tenant id is un-writable (returns ErrNotFound) rather than silently overwriting another tenant's document. The mismatch guard stays for callers that pass a doc whose TenantID differs from the write tenant.

type ImportJobRepository

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

ImportJobRepository persists the async document-import queue (design D7). Rows carry the uploaded archive as bytea plus progress counters a worker updates as it drains the queue.

func NewImportJobRepository

func NewImportJobRepository(db *gorm.DB) *ImportJobRepository

func (*ImportJobRepository) ClaimNext

func (r *ImportJobRepository) ClaimNext(ctx context.Context) (*models.ImportJob, error)

ClaimNext atomically claims the oldest queued job and flips it to running, using SELECT ... FOR UPDATE SKIP LOCKED so multiple worker replicas cooperate without ever double-processing a row (design: Risks — multi-replica worker). Returns (nil, nil) when the queue holds no claimable job.

func (*ImportJobRepository) Create

Create inserts a job (typically status=queued) with its archive bytes.

func (*ImportJobRepository) Finish

func (r *ImportJobRepository) Finish(ctx context.Context, id uuid.UUID, status, errMsg string, total, imported, skipped, failed int) error

Finish writes the terminal status (succeeded|failed), final counters, and the error string (empty on success). The write is guarded by WHERE status = running so a terminal row is never overwritten: once a startup sweep marks an orphaned job failed, a slower worker's Finish(succeeded) matches no row, is a no-op, and returns ErrNotFound. This makes the terminal state deterministic (failed wins), matching the interrupted->failed->retry semantics (design D9).

func (*ImportJobRepository) GetByID

func (r *ImportJobRepository) GetByID(ctx context.Context, id, tenantID uuid.UUID) (*models.ImportJob, error)

GetByID returns a job scoped to its owning tenant. ErrNotFound when the id is unknown or belongs to a different tenant — a job is visible only to its owner.

func (*ImportJobRepository) GetStatusByID

func (r *ImportJobRepository) GetStatusByID(ctx context.Context, id, tenantID uuid.UUID) (*models.ImportJob, error)

GetStatusByID returns a job WITHOUT its Archive blob — for the status/poll path, which never needs the (up to ~32MiB) archive. ClaimNext/GetByID keep loading the full row for the worker. Same tenant scoping and not-found mapping as GetByID: a job is visible only to its owning tenant.

func (*ImportJobRepository) SweepRunningToFailed

func (r *ImportJobRepository) SweepRunningToFailed(ctx context.Context) (int64, error)

SweepRunningToFailed reclaims only jobs stuck in `running` past staleRunningThreshold (a crashed process left the row orphaned). Called on worker start. It deliberately does NOT touch a job a live peer replica is actively processing: ClaimNext bumps updated_at at claim time (gorm auto-manages UpdatedAt), so a live import's row stays fresh and is skipped by the age guard — the operator sees a clean failure to retry only for genuinely orphaned rows, not for imports another replica is still running (D9, F3).

func (*ImportJobRepository) UpdateProgress

func (r *ImportJobRepository) UpdateProgress(ctx context.Context, id uuid.UUID, total, imported, skipped, failed int) error

UpdateProgress writes the counters of an in-flight job (e.g. seeding total at the start of processing) without changing its status. The write is guarded by WHERE status = running so it only mutates a job that is still running: if a peer replica's startup sweep already reclaimed the row (running->failed), the update is a no-op and ErrNotFound is returned, letting the caller tell "job no longer running" from success (design D9).

type IndexDepth

type IndexDepth string

IndexDepth controls the aggregation level of GenerateIndex output.

const (
	IndexDepthSummary  IndexDepth = "summary"
	IndexDepthCategory IndexDepth = "category"
	IndexDepthFull     IndexDepth = "full"
)

type IndexEntry

type IndexEntry struct {
	TenantID    uuid.UUID `json:"tenant_id"`
	TenantName  string    `json:"tenant_name,omitempty"`
	Category    string    `json:"category"`
	Subcategory *string   `json:"subcategory,omitempty"`
	DocCount    int       `json:"doc_count"`
	Topics      string    `json:"topics"`
}

IndexEntry is one row in the catalog produced by GenerateIndex.

type LintFinding

type LintFinding struct {
	Check        string       `json:"check"`
	Severity     LintSeverity `json:"severity"`
	DocumentPath string       `json:"document_path"`
	Message      string       `json:"message"`
}

type LintRepository

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

func NewLintRepository

func NewLintRepository(db *gorm.DB) *LintRepository

func (*LintRepository) CheckEmptyCategories

func (r *LintRepository) CheckEmptyCategories(ctx context.Context, tenantID uuid.UUID, thresholds LintThresholds) ([]LintFinding, error)

CheckEmptyCategories finds subcategories with fewer documents than the minimum threshold.

func (*LintRepository) CheckNearDuplicates

func (r *LintRepository) CheckNearDuplicates(ctx context.Context, tenantID uuid.UUID, thresholds LintThresholds) ([]LintFinding, error)

CheckNearDuplicates finds doc pairs sharing at least one near-duplicate section. Metric: MAX section-pair cosine per pair, above thresholds.DuplicateSimilarity.

func (*LintRepository) CheckSparse

func (r *LintRepository) CheckSparse(ctx context.Context, tenantID uuid.UUID, thresholds LintThresholds) ([]LintFinding, error)

CheckSparse finds documents with too few sections or insufficient content length.

func (*LintRepository) CheckStale

func (r *LintRepository) CheckStale(ctx context.Context, tenantID uuid.UUID, thresholds LintThresholds) ([]LintFinding, error)

CheckStale finds documents that have not been updated within the stale_days threshold.

func (*LintRepository) FindNearDuplicatePairs

func (r *LintRepository) FindNearDuplicatePairs(ctx context.Context, tenantID uuid.UUID, threshold float64) ([]NearDuplicatePair, error)

FindNearDuplicatePairs returns doc-ID pairs whose section-level cosine reaches the threshold. Metric: MAX(1 - cosine) over section pairs — "best matching section across the pair", more discriminating than doc-AVG. Feeds the cleanup queue.

Cost control (audit #14): scoped to the tenant's OWN docs only — the shared common pool is excluded, so cross-tenant override pairs (never auto-mergeable) aren't enqueued. Bounded via per-section HNSW k-NN, capped outer scan and result set, under a statement_timeout (runBoundedScan).

type LintSeverity

type LintSeverity string
const (
	LintSeverityWarning LintSeverity = "warning"
	LintSeverityInfo    LintSeverity = "info"
)

type LintThresholds

type LintThresholds struct {
	StaleDays            int     `json:"stale_days"`
	SparseMinSections    int     `json:"sparse_min_sections"`
	SparseMinContentLen  int     `json:"sparse_min_content_len"`
	DuplicateSimilarity  float64 `json:"duplicate_similarity"`
	EmptyCategoryMinDocs int     `json:"empty_category_min_docs"`

	// Bounds on the near-duplicate section self-join (audit #10). Zero means
	// "use the package default" so existing callers stay unaffected.
	DuplicateMaxSections int `json:"duplicate_max_sections,omitempty"`
	DuplicateNeighbors   int `json:"duplicate_neighbors,omitempty"`
	DuplicateMaxPairs    int `json:"duplicate_max_pairs,omitempty"`
}

func DefaultLintThresholds

func DefaultLintThresholds() LintThresholds

type NearDuplicatePair

type NearDuplicatePair struct {
	DocAID     uuid.UUID `gorm:"column:doc_a_id"`
	DocBID     uuid.UUID `gorm:"column:doc_b_id"`
	Similarity float64   `gorm:"column:similarity"`
}

NearDuplicatePair is a raw doc pair from the near-duplicate scan. The document IDs let the cleanup scanner upsert into the cleanup_queue table.

type OverrideEvent

type OverrideEvent struct {
	TenantID     uuid.UUID
	Tool         string
	TargetID     *uuid.UUID
	OverrideType string
	Reason       string
	APIKeyID     *uuid.UUID
}

type OverrideLogRepository

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

func NewOverrideLogRepository

func NewOverrideLogRepository(db *gorm.DB) *OverrideLogRepository

func (*OverrideLogRepository) Log

Log records a single override event. Best-effort — caller decides whether to fail on logging errors (usually not; a dropped audit row beats a dropped request).

type RelatedResult

type RelatedResult struct {
	DocumentID  uuid.UUID `json:"document_id"`
	Category    string    `json:"category"`
	Subcategory *string   `json:"subcategory,omitempty"`
	Slug        string    `json:"slug"`
	DocTitle    string    `json:"doc_title"`
	Similarity  float64   `json:"similarity"`

	// Owning-tenant label (cross-tenant reads). TenantID comes from SQL; Name and
	// Type are resolved by the service layer for the distinct result tenants.
	TenantID   uuid.UUID `json:"tenant_id"`
	TenantName string    `json:"tenant_name,omitempty"`
	TenantType string    `json:"tenant_type,omitempty"`
}

type RetentionRepository

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

RetentionRepository runs the retention sweep SQL: archive expired docs and hard-delete docs past the archive grace window. No LLM calls.

func NewRetentionRepository

func NewRetentionRepository(db *gorm.DB) *RetentionRepository

func (*RetentionRepository) ArchiveExpired

func (r *RetentionRepository) ArchiveExpired(ctx context.Context, tenantID uuid.UUID, cutoffs map[string]time.Time) (int64, error)

ArchiveExpired sets archived_at on docs whose freshest section is older than the doc_type cutoff. "Freshest" = latest of verified_at/updated_at/created_at, so editing a section (bumps updated_at, not verified_at) keeps a doc alive, not just mark_verified. One UPDATE per doc_type.

Zero-section fallback: a doc with NO sections satisfies NOT EXISTS trivially and would otherwise be archived on its very next sweep regardless of its own age. The extra guard makes the zero-section case fall back to the document's own created_at/updated_at, so a freshly-created empty doc is not archived until it is itself older than the cutoff. Docs that have sections are unaffected.

func (*RetentionRepository) DeleteArchived

func (r *RetentionRepository) DeleteArchived(ctx context.Context, tenantID uuid.UUID, before time.Time) (int64, error)

DeleteArchived hard-deletes docs archived before `before` in one atomic statement: writes a deletion_events audit row per victim, purges cleanup_queue rows referencing a victim, prunes the victims' relation_tuples, then deletes the docs (sections cascade). Returns count deleted.

type SearchParams

type SearchParams struct {
	TenantIDs   []uuid.UUID
	Embedding   pgvector.Vector
	Query       string
	Category    *string
	Subcategory *string
	Limit       int
}

SearchParams groups the inputs for hybrid search.

type SearchResult

type SearchResult struct {
	SectionID      uuid.UUID  `json:"section_id"`
	DocumentID     uuid.UUID  `json:"document_id"`
	Heading        *string    `json:"heading,omitempty"`
	Content        string     `json:"content,omitempty"`
	Score          float64    `json:"score"`
	Tier           string     `json:"relevance,omitempty"` // high | standard | low — calibrated from Score
	Category       string     `json:"category"`
	Subcategory    *string    `json:"subcategory,omitempty"`
	Slug           string     `json:"slug"`
	DocTitle       string     `json:"doc_title"`
	DocType        string     `json:"doc_type,omitempty"`
	VerifiedAt     *time.Time `json:"verified_at,omitempty"`
	SectionCreated time.Time  `json:"-"`

	// Owning-tenant label (cross-tenant reads). TenantID comes from SQL; Name and
	// Type are resolved by the service layer for the distinct result tenants.
	TenantID   uuid.UUID `json:"tenant_id"`
	TenantName string    `json:"tenant_name,omitempty"`
	TenantType string    `json:"tenant_type,omitempty"`

	// Staleness overlay (set by service layer after fetch, not by SQL).
	Status        string   `json:"status,omitempty"`         // "needs_verification" when guarded
	Preview       string   `json:"preview,omitempty"`        // short preview of withheld content
	VerifyHints   []string `json:"verify_hints,omitempty"`   // code paths to check
	StaleDays     int      `json:"age_days,omitempty"`       // age of verified_at in days
	ThresholdDays int      `json:"threshold_days,omitempty"` // threshold for this doc_type
}

type SectionRepository

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

func NewSectionRepository

func NewSectionRepository(db *gorm.DB) *SectionRepository

func (*SectionRepository) CreateBatch

func (r *SectionRepository) CreateBatch(ctx context.Context, sections []models.Section) error

func (*SectionRepository) DeleteByDocumentID

func (r *SectionRepository) DeleteByDocumentID(ctx context.Context, docID uuid.UUID) error

func (*SectionRepository) FindSimilarDocuments

func (r *SectionRepository) FindSimilarDocuments(
	ctx context.Context,
	tenantID uuid.UUID,
	newEmbeddings []pgvector.Vector,
	threshold float64,
	limit int,
	excludeCategory string,
	excludeSubcategory *string,
	excludeSlug string,
) ([]SimilarityCandidate, error)

FindSimilarDocuments scans the WRITE tenant's OWN existing sections and returns documents whose best section match against any of newEmbeddings meets the threshold. It deliberately excludes the common (bootstrap) pool: this backs the write-time duplicate guard, and a caller can't edit or merge common-pool docs, so flagging a common-pool near-duplicate would block a legitimate write against an un-editable doc. Pass excludeCategory/Subcategory/Slug so updating an existing doc doesn't self-flag. Returns up to `limit`, ordered by similarity desc. Runs as one query via a CROSS JOIN over a Postgres vector array literal.

func (*SectionRepository) GetByID

func (r *SectionRepository) GetByID(ctx context.Context, tenantID uuid.UUID, id uuid.UUID) (*models.Section, error)

func (*SectionRepository) GetRelated

func (r *SectionRepository) GetRelated(ctx context.Context, tenantIDs []uuid.UUID, documentID uuid.UUID, limit int) ([]RelatedResult, error)

GetRelated returns documents semantically related to documentID, restricted to the caller's readable tenant set (tenant_id IN tenantIDs) so no result can leak a tenant outside that set. The service layer computes tenantIDs via readScope/readableTenants and resolves the per-result tenant labels.

func (*SectionRepository) HybridSearch

func (r *SectionRepository) HybridSearch(ctx context.Context, p SearchParams) ([]SearchResult, error)

HybridSearch gathers vector and lexical candidates (scope-filtered, capped) via a FULL OUTER JOIN so lexical-only matches are recalled, then fuses them in Go. Scope filters run inside both CTEs, before ranking.

func (*SectionRepository) MarkVerified

func (r *SectionRepository) MarkVerified(ctx context.Context, tenantID uuid.UUID, id uuid.UUID) error

MarkVerified sets verified_at = NOW() if the section belongs to one of the caller's accessible tenants. ErrNotFound if not in scope.

func (*SectionRepository) Update

func (r *SectionRepository) Update(ctx context.Context, section *models.Section) error

type SimilarityCandidate

type SimilarityCandidate struct {
	DocumentID    uuid.UUID `json:"document_id"`
	Category      string    `json:"category"`
	Subcategory   *string   `json:"subcategory,omitempty"`
	Slug          string    `json:"slug"`
	Title         string    `json:"title"`
	MaxSimilarity float64   `json:"similarity"`
}

SimilarityCandidate is an existing document that may collide with a new save. MaxSimilarity is the cosine (0..1) of the best-matching section pair between the new embeddings and the candidate's existing sections.

func (SimilarityCandidate) Path

func (c SimilarityCandidate) Path() string

Path returns the candidate's hierarchical path string.

type TenantRepository

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

func NewTenantRepository

func NewTenantRepository(db *gorm.DB) *TenantRepository

func (*TenantRepository) Create

func (r *TenantRepository) Create(ctx context.Context, tenant *models.Tenant) error

func (*TenantRepository) Delete

func (r *TenantRepository) Delete(ctx context.Context, id uuid.UUID) error

Delete removes a tenant and everything scoped to it in one transaction: the authz relation tuples (document, tenant, and service-principal), its documents (with their sections), API keys, and the per-tenant bookkeeping tables (import_jobs, cleanup_queue, override_log, deletion_events). tenant_users cascade via their FK. Leaving any of these behind orphans rows and — for the tuples — leaves live authorization grants pointing at a tenant that no longer exists.

func (*TenantRepository) GetByID

func (r *TenantRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Tenant, error)

func (*TenantRepository) GetByIDs

func (r *TenantRepository) GetByIDs(ctx context.Context, ids []uuid.UUID) ([]models.Tenant, error)

GetByIDs fetches the tenants for the given ids in one query. Used by the read path to label cross-tenant results with their owning tenant's name/type without an N+1. Missing ids are simply absent from the result.

func (*TenantRepository) List

func (r *TenantRepository) List(ctx context.Context) ([]models.Tenant, error)

func (*TenantRepository) Update

func (r *TenantRepository) Update(ctx context.Context, tenant *models.Tenant) error

Jump to

Keyboard shortcuts

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