Help us shape the future of Go! Take the 2026 Go Developer Survey

datacatalog

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 9 Imported by: 0

README

DataCatalog

Go CI Go Lint Go SAST Docs License

A small, dependency-free data catalog for Go: a model that describes which entities and fields an application stores and how each one is classified.

Why

Several concerns need the same facts about stored data, and each tends to re-invent them as scattered tags and allow-lists:

  • a query guard needs to know which fields user-authored queries may touch;
  • privacy tooling needs to know which entities hold personal data, how to find a data subject's rows, and what to do with each field on erasure;
  • documentation and governance need an inventory, ownership and retention notes.

DataCatalog is the single, validated description they can all read. It is plain data with a handful of predicates, so the catalog can be authored in Go or JSON, checked in CI, and consumed by whatever needs it.

What it is not

The core package depends only on the standard library. It has no database driver, ORM or query-language dependency. Adapters for ORMs and query guards live in their own modules; they import this one, not the other way round.

Model

Type Purpose
Catalog A named set of entities.
Entity One record type: Name, Owner, Description, SubjectKey, PrivacyScope, HoldApplicable, Versioned, Retention, Fields.
Field One attribute: Name, Description, Type, Visibility, PII, Secret, Erasure, RetainReason, Export.
  • Type is coarse: string, number, bool, time, json, bytes.
  • Visibility is public or internal and has no default: an unset visibility is a validation error, so every field is classified on purpose.
  • Erasure is one of clear, tombstone, hash, delete_row, shred, retain.
  • PrivacyScope, HoldApplicable and Versioned are entity-level: they describe the record, not a column.

Validate() reports all problems at once, each with a stable Rule:

Rule Meaning
name_required, name_unique Catalog, entity and field names are non-empty; entity names are unique in a catalog and field names within an entity.
visibility_required Visibility is explicitly public or internal.
secret_internal A secret field must be internal.
subject_key_required / subject_key_unknown A privacy-scope entity with PII needs a SubjectKey, and a SubjectKey must name an existing field.
pii_erasure_required A PII field in a privacy-scope entity needs an erasure strategy.
retain_reason_required Erasure retain needs a RetainReason.
type_invalid, erasure_invalid Optional enumerations, when set, hold a known value.

Example

cat := &datacatalog.Catalog{
    Name: "shop",
    Entities: []datacatalog.Entity{{
        Name:         "account",
        Owner:        "identity",
        SubjectKey:   "id",
        PrivacyScope: true,
        Fields: []datacatalog.Field{
            {Name: "id", Type: datacatalog.TypeString,
                Visibility: datacatalog.VisibilityPublic, Export: true},
            // PII: queryable only when the caller opts in; cleared on erasure.
            {Name: "email", Type: datacatalog.TypeString,
                Visibility: datacatalog.VisibilityPublic,
                PII: true, Erasure: datacatalog.ErasureClear, Export: true},
            // Secret: never queryable, never exported; the key is shredded.
            {Name: "password_hash", Type: datacatalog.TypeBytes,
                Visibility: datacatalog.VisibilityInternal,
                Secret: true, Erasure: datacatalog.ErasureShred},
            // Internal flag: hidden from the application audience.
            {Name: "is_staff", Type: datacatalog.TypeBool,
                Visibility: datacatalog.VisibilityInternal},
        },
    }},
}

if err := cat.Validate(); err != nil {
    log.Fatal(err) // lists every problem
}

cat.AllowedInQuery("account", "id", false)            // true
cat.AllowedInQuery("account", "email", false)         // false: PII
cat.AllowedInQuery("account", "email", true)          // true
cat.AllowedInQuery("account", "password_hash", true)  // false: secret
cat.AllowedInQuery("account", "is_staff", true)       // false: internal
cat.AllowedInQuery("account", "missing", true)        // false: default-deny

The same catalog as JSON (entities are written sorted by name so diffs stay small):

{
  "name": "shop",
  "entities": [
    {
      "name": "account",
      "owner": "identity",
      "subject_key": "id",
      "privacy_scope": true,
      "fields": [
        {"name": "id", "type": "string", "visibility": "public", "export": true},
        {"name": "email", "type": "string", "visibility": "public", "pii": true, "erasure": "clear", "export": true},
        {"name": "password_hash", "type": "bytes", "visibility": "internal", "secret": true, "erasure": "shred"},
        {"name": "is_staff", "type": "bool", "visibility": "internal"}
      ]
    }
  ]
}

Using it

Query guard. Give the guard a field policy backed by the catalog. The predicate is default-deny: unknown entities and fields are rejected, internal and secret fields are always blocked, and PII is blocked unless the caller is allowed to see it.

func (g *Guard) FieldAllowed(entity, field string) bool {
    return g.cat.AllowedInQuery(entity, field, g.allowPII)
}

Ent-based (or any ORM) application. Keep the catalog next to the schema, validate it in a test, and let a small adapter module check that the catalog and the generated schema agree (every Ent field is classified, no stale entries). At runtime, privacy code walks PrivacyScopeEntities(), finds a subject's rows through SubjectKey, and applies each PII field's Erasure strategy.

Documentation. cat.RenderMarkdown() produces one section per entity with a field table, suitable for embedding in a docs site.

Many services. datacatalog.Merge(a, b, c) combines catalogs; identical definitions of the same entity collapse and conflicting ones are an error.

JSON Schema

The document format has a JSON Schema generated from the Go types (the Go structs are the source of truth). It is embedded and available as schema.JSON from github.com/grokify/datacatalog/schema, and committed at schema/datacatalog.schema.json. Regenerate with go generate ./schema; a test fails if the committed file is stale.

More

See the guides for the validation and erasure reference.

License

MIT. See LICENSE.

Documentation

Overview

Package datacatalog is a small, dependency-free data catalog: a model for describing which entities and fields an application stores and how each field is classified (visibility, PII, secret, erasure strategy).

The catalog is plain data. It is meant to be authored (in Go or JSON), validated once, and then consumed by query guards, privacy tooling (access and erasure requests), documentation generators and management tooling. Adapters for ORMs and query guards live in their own modules; this package depends only on the standard library.

Typical use:

cat, err := datacatalog.ReadFile("catalog.json")
if err != nil { ... }
if err := cat.Validate(); err != nil { ... } // reports every problem
if cat.AllowedInQuery("account", "email", false) { ... }
Example
package main

import (
	"fmt"

	"github.com/grokify/datacatalog"
)

func main() {
	cat := &datacatalog.Catalog{
		Name: "shop",
		Entities: []datacatalog.Entity{{
			Name:         "account",
			Owner:        "identity",
			SubjectKey:   "id",
			PrivacyScope: true,
			Fields: []datacatalog.Field{
				{Name: "id", Type: datacatalog.TypeString, Visibility: datacatalog.VisibilityPublic, Export: true},
				{Name: "email", Type: datacatalog.TypeString, Visibility: datacatalog.VisibilityPublic,
					PII: true, Erasure: datacatalog.ErasureClear, Export: true},
				{Name: "password_hash", Type: datacatalog.TypeBytes, Visibility: datacatalog.VisibilityInternal,
					Secret: true, Erasure: datacatalog.ErasureShred},
				{Name: "is_staff", Type: datacatalog.TypeBool, Visibility: datacatalog.VisibilityInternal},
			},
		}},
	}
	if err := cat.Validate(); err != nil {
		fmt.Println(err)
		return
	}
	for _, name := range []string{"id", "email", "password_hash", "is_staff"} {
		fmt.Printf("%-13s no PII: %-5v PII ok: %v\n", name,
			cat.AllowedInQuery("account", name, false), cat.AllowedInQuery("account", name, true))
	}
}
Output:
id            no PII: true  PII ok: true
email         no PII: false PII ok: true
password_hash no PII: false PII ok: false
is_staff      no PII: false PII ok: false

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Audience

type Audience string

Audience is who is looking at the catalog or issuing a query.

const (
	// Application is application code and user-facing queries. It sees only
	// public, non-secret fields.
	Application Audience = "application"
	// Internal is platform code. It sees every field.
	Internal Audience = "internal"
)

Audiences.

type Catalog

type Catalog struct {
	Name     string   `json:"name" jsonschema_description:"Catalog name, typically the application or service."`
	Entities []Entity `json:"entities,omitempty"`
}

Catalog is a named set of entities.

func Decode

func Decode(r io.Reader) (*Catalog, error)

Decode reads a JSON catalog. Unknown fields are rejected so typos in classifications are not silently ignored. Decode does not validate; call Validate on the result.

func Merge

func Merge(catalogs ...*Catalog) (*Catalog, error)

Merge combines catalogs into a new one, for example one catalog per service into an organization-wide view. The merged catalog takes the name of the first catalog that has one. Nil catalogs are skipped.

An entity defined in more than one catalog must be defined identically (the duplicate is then collapsed); otherwise Merge returns an error naming every conflicting entity. The result has entities sorted by name.

func Parse

func Parse(data []byte) (*Catalog, error)

Parse decodes a JSON catalog from bytes. See Decode.

func ReadFile

func ReadFile(path string) (*Catalog, error)

ReadFile loads a JSON catalog from a file. See Decode.

func (*Catalog) AllowedInQuery

func (c *Catalog) AllowedInQuery(entity, field string, allowPII bool) bool

AllowedInQuery is the default-deny lookup for a query guard: unknown entities and fields are not allowed.

func (*Catalog) Clone

func (c *Catalog) Clone() *Catalog

Clone returns a deep copy of the catalog.

func (*Catalog) Encode

func (c *Catalog) Encode(w io.Writer) error

Encode writes the catalog as stable indented JSON. See MarshalJSONIndent.

func (*Catalog) Entity

func (c *Catalog) Entity(name string) (Entity, bool)

Entity looks up an entity by name.

func (*Catalog) Field

func (c *Catalog) Field(entity, name string) (Field, bool)

Field looks up a field by entity and field name.

func (*Catalog) MarshalJSONIndent

func (c *Catalog) MarshalJSONIndent() ([]byte, error)

MarshalJSONIndent returns the catalog as indented JSON with entities sorted by name and a trailing newline, so output is stable and diff-friendly. The receiver is not modified.

func (*Catalog) Normalize

func (c *Catalog) Normalize()

Normalize sorts entities by name, in place. Field order is kept because declaration order is meaningful (it usually mirrors column order).

func (*Catalog) PrivacyScopeEntities

func (c *Catalog) PrivacyScopeEntities() []Entity

PrivacyScopeEntities returns the entities subject to access and erasure requests, in catalog order.

func (*Catalog) RenderMarkdown

func (c *Catalog) RenderMarkdown() string

RenderMarkdown renders the catalog as Markdown documentation: one section per entity (sorted by name) with a field table. The receiver is not modified and output is deterministic.

func (*Catalog) Validate

func (c *Catalog) Validate() error

Validate checks the catalog against every rule and returns a *ValidationError listing all problems, or nil when the catalog is valid.

func (*Catalog) WriteFile

func (c *Catalog) WriteFile(path string) error

WriteFile saves the catalog as stable indented JSON.

type Entity

type Entity struct {
	Name        string `json:"name" jsonschema_description:"Entity name; unique within the catalog."`
	Owner       string `json:"owner,omitempty" jsonschema_description:"Team or component responsible for the entity."`
	Description string `json:"description,omitempty"`
	// SubjectKey names the field that identifies the data subject.
	SubjectKey string `json:"subject_key,omitempty" jsonschema_description:"SubjectKey names the field that identifies the data subject."`
	// PrivacyScope marks the entity as subject to access and erasure requests.
	PrivacyScope bool `` /* 128-byte string literal not displayed */
	// HoldApplicable marks the entity as subject to legal holds. It is an
	// entity-level property: holds suspend erasure for whole rows.
	HoldApplicable bool `` /* 186-byte string literal not displayed */
	// Versioned marks the entity as keeping row history. It is an
	// entity-level property: history rows must be erased with their subject.
	Versioned bool `` /* 182-byte string literal not displayed */
	// Retention is a free-text retention note.
	Retention string  `json:"retention,omitempty" jsonschema_description:"Retention is a free-text retention note."`
	Fields    []Field `json:"fields,omitempty"`
}

Entity describes one stored record type (a table, collection or document kind) and its fields.

func (Entity) Field

func (e Entity) Field(name string) (Field, bool)

Field looks up a field by name.

func (Entity) HasPII

func (e Entity) HasPII() bool

HasPII reports whether any field of the entity is PII.

func (Entity) PIIFields

func (e Entity) PIIFields() []Field

PIIFields returns the PII fields in declaration order.

func (Entity) QueryableFields

func (e Entity) QueryableFields(allowPII bool) []Field

QueryableFields returns the fields for which AllowedInQuery(allowPII) holds.

func (Entity) Validate

func (e Entity) Validate() error

Validate checks a single entity against the entity and field rules.

func (Entity) VisibleFields

func (e Entity) VisibleFields(aud Audience) []Field

VisibleFields returns the fields visible to aud, in declaration order.

type Erasure

type Erasure string

Erasure is the strategy applied to a field when its data subject is erased.

const (
	// ErasureClear sets the value to its zero value or NULL.
	ErasureClear Erasure = "clear"
	// ErasureTombstone replaces the value with a fixed marker.
	ErasureTombstone Erasure = "tombstone"
	// ErasureHash replaces the value with a one-way hash.
	ErasureHash Erasure = "hash"
	// ErasureDeleteRow removes the whole row.
	ErasureDeleteRow Erasure = "delete_row"
	// ErasureShred destroys the key that protects the value (crypto-shredding).
	ErasureShred Erasure = "shred"
	// ErasureRetain keeps the value; RetainReason must say why.
	ErasureRetain Erasure = "retain"
)

Erasure strategies.

func (Erasure) Valid

func (e Erasure) Valid() bool

Valid reports whether e is a known strategy. The empty Erasure is not valid; it means "no strategy declared".

type Field

type Field struct {
	Name        string     `json:"name" jsonschema_description:"Field (column) name; unique within the entity."`
	Description string     `json:"description,omitempty"`
	Type        Type       `json:"type,omitempty" jsonschema:"enum=string,enum=number,enum=bool,enum=time,enum=json,enum=bytes"`
	Visibility  Visibility `` /* 136-byte string literal not displayed */
	// PII marks personal data about the data subject.
	PII bool `json:"pii,omitempty" jsonschema_description:"PII marks personal data about the data subject."`
	// Secret marks credentials and key material; it implies internal visibility.
	Secret bool `json:"secret,omitempty" jsonschema_description:"Secret marks credentials and key material; it implies internal visibility."`
	// Erasure is the strategy applied when the data subject is erased.
	Erasure Erasure `` /* 202-byte string literal not displayed */
	// RetainReason is required when Erasure is "retain".
	RetainReason string `json:"retain_reason,omitempty" jsonschema_description:"RetainReason is required when Erasure is 'retain'."`
	// Export includes the field in data-subject access exports.
	Export bool `json:"export,omitempty" jsonschema_description:"Export includes the field in data-subject access exports."`
}

Field describes one stored attribute of an Entity.

func (Field) AllowedInQuery

func (f Field) AllowedInQuery(allowPII bool) bool

AllowedInQuery reports whether a query guard may let the field appear in a user-authored query. Internal and secret fields are always blocked; PII fields are blocked unless allowPII is true.

func (Field) VisibleTo

func (f Field) VisibleTo(aud Audience) bool

VisibleTo reports whether the field is visible to the audience. Secret fields are never visible to Application, even if misclassified as public.

type Problem

type Problem struct {
	Entity  string `json:"entity,omitempty"`
	Field   string `json:"field,omitempty"`
	Rule    Rule   `json:"rule"`
	Message string `json:"message"`
}

Problem is a single validation finding. Entity and Field are empty when the problem is not about a specific entity or field.

func (Problem) String

func (p Problem) String() string

String renders the problem as "entity.field: message (rule)".

type Rule

type Rule string

Rule identifies a validation rule so callers can react to specific problems without parsing messages.

const (
	RuleNameRequired        Rule = "name_required"
	RuleNameUnique          Rule = "name_unique"
	RuleVisibilityRequired  Rule = "visibility_required"
	RuleTypeInvalid         Rule = "type_invalid"
	RuleErasureInvalid      Rule = "erasure_invalid"
	RuleSecretInternal      Rule = "secret_internal"
	RuleSubjectKeyRequired  Rule = "subject_key_required"
	RuleSubjectKeyUnknown   Rule = "subject_key_unknown"
	RulePIIErasureRequired  Rule = "pii_erasure_required"
	RuleRetainReasonMissing Rule = "retain_reason_required"
)

Validation rules.

type Type

type Type string

Type is the coarse storage type of a field. It is descriptive, not a database type: adapters map it to whatever the engine needs.

const (
	TypeString Type = "string"
	TypeNumber Type = "number"
	TypeBool   Type = "bool"
	TypeTime   Type = "time"
	TypeJSON   Type = "json"
	TypeBytes  Type = "bytes"
)

Field types.

func (Type) Valid

func (t Type) Valid() bool

Valid reports whether t is a known type. The empty Type is not valid; callers that treat the type as optional check for "" first.

type ValidationError

type ValidationError struct {
	Problems []Problem
}

ValidationError carries every problem found by Validate.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error lists all problems, one per line.

func (*ValidationError) Has

func (e *ValidationError) Has(r Rule) bool

Has reports whether any problem matches the rule.

type Visibility

type Visibility string

Visibility says who may see a field. There is deliberately no default: the zero value is invalid so that every field is classified explicitly.

const (
	// VisibilityPublic fields may be exposed to application code and
	// user-facing queries.
	VisibilityPublic Visibility = "public"
	// VisibilityInternal fields are for the platform only.
	VisibilityInternal Visibility = "internal"
)

Field visibilities.

func (Visibility) Valid

func (v Visibility) Valid() bool

Valid reports whether v is an explicit, known visibility.

Directories

Path Synopsis
Package schema embeds the JSON Schema for the catalog document format.
Package schema embeds the JSON Schema for the catalog document format.

Jump to

Keyboard shortcuts

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