pdbcompat

package
v1.18.14 Latest Latest
Warning

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

Go to latest
Published: May 3, 2026 License: BSD-3-Clause Imports: 41 Imported by: 0

Documentation

Overview

Package pdbcompat provides a PeeringDB-compatible REST API layer that translates Django-style query parameters to ent predicates and serializes ent entities to PeeringDB's exact JSON response format.

Index

Constants

View Source
const (
	// DefaultLimit is the default value used when the `limit=` query
	// parameter is absent. Set to 0 ("unlimited") to mirror upstream
	// PeeringDB's `rest.py:495` behaviour: bare `/api/<type>` URLs
	// return ALL rows from the queryset, not a paginated page.
	//
	// Earlier revisions of this code defaulted to 250, treating that as
	// a defensive page-size cap. The cap turned out to be a real parity
	// bug — verified 2026-04-28 against upstream live data
	// (parity-results.txt: bare /api/org returned 33,556 rows upstream
	// vs 250 on the mirror). The Phase 71 response-memory budget
	// (PDBPLUS_RESPONSE_MEMORY_LIMIT, default 128 MiB) is the real DoS
	// safeguard — it gates the precount × TypicalRowBytes before
	// materialising any result set, returning 413 application/problem+json
	// when the would-be payload exceeds the budget. The 250 default
	// added nothing on top of that, only divergence.
	DefaultLimit = 0

	// MaxLimit is the maximum allowed page size per D-21. Applied only
	// when the caller supplies an explicit positive `limit=` (see
	// ParsePaginationParams). limit=0 / unset bypasses this clamp and
	// returns the full result set, gated by the Phase 71 budget.
	MaxLimit = 1000
)
View Source
const FlushEvery = 100

FlushEvery is the row count between periodic http.Flusher.Flush() calls. Provides chunked backpressure for large payloads without the per-row syscall overhead of flushing after every Write.

View Source
const ResponseTooLargeType = "https://peeringdb-plus.fly.dev/errors/response-too-large"

ResponseTooLargeType is the RFC 9457 problem-type URI for pre-flight budget-exceeded 413 responses (Phase 71 D-04). Package constant so Plan 04's handler wiring and Plan 72's parity tests reference the same literal — drift between the wire value and the documented value is a silent compatibility break.

Variables

View Source
var Allowlists = map[string]AllowlistEntry{
	"campus": {
		Direct: []string{
			"fac__country",
			"fac__name",
			"org__name",
		},
	},
	"carrier": {
		Direct: []string{
			"fac__country",
			"fac__name",
			"org__name",
		},
	},
	"carrierfac": {
		Direct: []string{
			"carrier__name",
			"fac__country",
			"fac__name",
		},
	},
	"fac": {
		Direct: []string{
			"campus__name",
			"ix__id",
			"ix__name",
			"net__asn",
			"net__name",
			"org__name",
		},
		Via: map[string][]string{
			"ixlan": {
				"ix__fac_count",
			},
		},
	},
	"ix": {
		Direct: []string{
			"fac__country",
			"fac__name",
			"ixlan__name",
			"ixpfx__prefix",
			"net__asn",
			"net__name",
			"org__name",
		},
	},
	"ixfac": {
		Direct: []string{
			"fac__city",
			"fac__country",
			"fac__name",
			"ix__name",
		},
	},
	"ixlan": {
		Direct: []string{
			"ix__id",
			"ix__name",
			"ixpfx__prefix",
		},
	},
	"ixpfx": {
		Direct: []string{
			"ixlan__name",
		},
		Via: map[string][]string{
			"ixlan": {
				"ix__id",
				"ix__name",
			},
		},
	},
	"net": {
		Direct: []string{
			"fac__name",
			"ix__name",
			"ixlan__name",
			"org__id",
			"org__name",
		},
		Via: map[string][]string{
			"netfac": {
				"fac__name",
			},
		},
	},
	"netfac": {
		Direct: []string{
			"fac__country",
			"fac__name",
			"net__asn",
			"net__name",
		},
	},
	"netixlan": {
		Direct: []string{
			"ix__id",
			"ix__name",
			"ixlan__name",
			"net__asn",
			"net__name",
		},
	},
	"org": {
		Direct: []string{
			"fac__country",
			"fac__name",
			"ix__name",
			"net__asn",
			"net__name",
		},
	},
	"poc": {
		Direct: []string{
			"net__asn",
			"net__name",
		},
	},
}

Allowlists maps a PeeringDB type name (e.g. "net") to its Path A allowlist — the set of <fk>__<field> and <fk>__<fk>__<field> keys that mirror upstream serializers.py get_relation_filters(...) lists.

View Source
var Edges = map[string][]EdgeMetadata{
	"campus": {
		{Name: "facilities", TargetType: "fac", TraversalKey: "fac", Excluded: false, ParentFKColumn: "campus_id", TargetTable: "facilities", TargetIDColumn: "id", OwnFK: false},
		{Name: "organization", TargetType: "org", TraversalKey: "org", Excluded: false, ParentFKColumn: "org_id", TargetTable: "organizations", TargetIDColumn: "id", OwnFK: true},
	},
	"carrier": {
		{Name: "carrier_facilities", TargetType: "carrierfac", TraversalKey: "carrierfac", Excluded: false, ParentFKColumn: "carrier_id", TargetTable: "carrier_facilities", TargetIDColumn: "id", OwnFK: false},
		{Name: "organization", TargetType: "org", TraversalKey: "org", Excluded: false, ParentFKColumn: "org_id", TargetTable: "organizations", TargetIDColumn: "id", OwnFK: true},
	},
	"carrierfac": {
		{Name: "carrier", TargetType: "carrier", TraversalKey: "carrier", Excluded: false, ParentFKColumn: "carrier_id", TargetTable: "carriers", TargetIDColumn: "id", OwnFK: true},
		{Name: "facility", TargetType: "fac", TraversalKey: "fac", Excluded: false, ParentFKColumn: "fac_id", TargetTable: "facilities", TargetIDColumn: "id", OwnFK: true},
	},
	"fac": {
		{Name: "campus", TargetType: "campus", TraversalKey: "campus", Excluded: false, ParentFKColumn: "campus_id", TargetTable: "campuses", TargetIDColumn: "id", OwnFK: true},
		{Name: "carrier_facilities", TargetType: "carrierfac", TraversalKey: "carrierfac", Excluded: false, ParentFKColumn: "fac_id", TargetTable: "carrier_facilities", TargetIDColumn: "id", OwnFK: false},
		{Name: "ix_facilities", TargetType: "ixfac", TraversalKey: "ixfac", Excluded: false, ParentFKColumn: "fac_id", TargetTable: "ix_facilities", TargetIDColumn: "id", OwnFK: false},
		{Name: "network_facilities", TargetType: "netfac", TraversalKey: "netfac", Excluded: false, ParentFKColumn: "fac_id", TargetTable: "network_facilities", TargetIDColumn: "id", OwnFK: false},
		{Name: "organization", TargetType: "org", TraversalKey: "org", Excluded: false, ParentFKColumn: "org_id", TargetTable: "organizations", TargetIDColumn: "id", OwnFK: true},
	},
	"ix": {
		{Name: "ix_facilities", TargetType: "ixfac", TraversalKey: "ixfac", Excluded: false, ParentFKColumn: "ix_id", TargetTable: "ix_facilities", TargetIDColumn: "id", OwnFK: false},
		{Name: "ix_lans", TargetType: "ixlan", TraversalKey: "ixlan", Excluded: false, ParentFKColumn: "ix_id", TargetTable: "ix_lans", TargetIDColumn: "id", OwnFK: false},
		{Name: "organization", TargetType: "org", TraversalKey: "org", Excluded: false, ParentFKColumn: "org_id", TargetTable: "organizations", TargetIDColumn: "id", OwnFK: true},
	},
	"ixfac": {
		{Name: "facility", TargetType: "fac", TraversalKey: "fac", Excluded: false, ParentFKColumn: "fac_id", TargetTable: "facilities", TargetIDColumn: "id", OwnFK: true},
		{Name: "internet_exchange", TargetType: "ix", TraversalKey: "ix", Excluded: false, ParentFKColumn: "ix_id", TargetTable: "internet_exchanges", TargetIDColumn: "id", OwnFK: true},
	},
	"ixlan": {
		{Name: "internet_exchange", TargetType: "ix", TraversalKey: "ix", Excluded: false, ParentFKColumn: "ix_id", TargetTable: "internet_exchanges", TargetIDColumn: "id", OwnFK: true},
		{Name: "ix_prefixes", TargetType: "ixpfx", TraversalKey: "ixpfx", Excluded: false, ParentFKColumn: "ixlan_id", TargetTable: "ix_prefixes", TargetIDColumn: "id", OwnFK: false},
		{Name: "network_ix_lans", TargetType: "netixlan", TraversalKey: "netixlan", Excluded: false, ParentFKColumn: "ixlan_id", TargetTable: "network_ix_lans", TargetIDColumn: "id", OwnFK: false},
	},
	"ixpfx": {
		{Name: "ix_lan", TargetType: "ixlan", TraversalKey: "ixlan", Excluded: false, ParentFKColumn: "ixlan_id", TargetTable: "ix_lans", TargetIDColumn: "id", OwnFK: true},
	},
	"net": {
		{Name: "network_facilities", TargetType: "netfac", TraversalKey: "netfac", Excluded: false, ParentFKColumn: "net_id", TargetTable: "network_facilities", TargetIDColumn: "id", OwnFK: false},
		{Name: "network_ix_lans", TargetType: "netixlan", TraversalKey: "netixlan", Excluded: false, ParentFKColumn: "net_id", TargetTable: "network_ix_lans", TargetIDColumn: "id", OwnFK: false},
		{Name: "organization", TargetType: "org", TraversalKey: "org", Excluded: false, ParentFKColumn: "org_id", TargetTable: "organizations", TargetIDColumn: "id", OwnFK: true},
		{Name: "pocs", TargetType: "poc", TraversalKey: "poc", Excluded: false, ParentFKColumn: "net_id", TargetTable: "pocs", TargetIDColumn: "id", OwnFK: false},
	},
	"netfac": {
		{Name: "facility", TargetType: "fac", TraversalKey: "fac", Excluded: false, ParentFKColumn: "fac_id", TargetTable: "facilities", TargetIDColumn: "id", OwnFK: true},
		{Name: "network", TargetType: "net", TraversalKey: "net", Excluded: false, ParentFKColumn: "net_id", TargetTable: "networks", TargetIDColumn: "id", OwnFK: true},
	},
	"netixlan": {
		{Name: "ix_lan", TargetType: "ixlan", TraversalKey: "ixlan", Excluded: false, ParentFKColumn: "ixlan_id", TargetTable: "ix_lans", TargetIDColumn: "id", OwnFK: true},
		{Name: "network", TargetType: "net", TraversalKey: "net", Excluded: false, ParentFKColumn: "net_id", TargetTable: "networks", TargetIDColumn: "id", OwnFK: true},
	},
	"org": {
		{Name: "campuses", TargetType: "campus", TraversalKey: "campus", Excluded: false, ParentFKColumn: "org_id", TargetTable: "campuses", TargetIDColumn: "id", OwnFK: false},
		{Name: "carriers", TargetType: "carrier", TraversalKey: "carrier", Excluded: false, ParentFKColumn: "org_id", TargetTable: "carriers", TargetIDColumn: "id", OwnFK: false},
		{Name: "facilities", TargetType: "fac", TraversalKey: "fac", Excluded: false, ParentFKColumn: "org_id", TargetTable: "facilities", TargetIDColumn: "id", OwnFK: false},
		{Name: "internet_exchanges", TargetType: "ix", TraversalKey: "ix", Excluded: false, ParentFKColumn: "org_id", TargetTable: "internet_exchanges", TargetIDColumn: "id", OwnFK: false},
		{Name: "networks", TargetType: "net", TraversalKey: "net", Excluded: false, ParentFKColumn: "org_id", TargetTable: "networks", TargetIDColumn: "id", OwnFK: false},
	},
	"poc": {
		{Name: "network", TargetType: "net", TraversalKey: "net", Excluded: false, ParentFKColumn: "net_id", TargetTable: "networks", TargetIDColumn: "id", OwnFK: true},
	},
}

Edges maps a PeeringDB type name (e.g. "net") to a slice of EdgeMetadata describing its outgoing ent edges. Consumed at request time by internal/pdbcompat.LookupEdge for Path B traversal.

Phase 70 D-02 (amended 2026-04-19): the map is emitted at `go generate` time from gen.Graph — no runtime client.Schema walk, no sync.Once, no init-order coupling. Freshness is enforced by the existing go-generate drift-check CI gate (same precedent as v1.15 Phase 63 hygiene drops).

TraversalKey is the <fk> token in filter params (equals TargetType today). Excluded edges (WithFilterExcludeFromTraversal annotation) are emitted with Excluded=true; LookupEdge hides them from its callers so consumers see them as missing.

ParentFKColumn, TargetTable, TargetIDColumn carry SQL-level metadata for Plan 70-05's subquery construction. Edges whose FK column or target table could not be resolved at codegen time are logged and skipped entirely (never emitted with blank metadata).

View Source
var FilterExcludes = map[string]map[string]bool{}

FilterExcludes mirrors upstream serializers.py:128-157 FILTER_EXCLUDE. Outer key: entity Go name (e.g. "Network"). Inner key: edge name (e.g. "pocs"). Value is always true; the map is used as a set.

View Source
var Registry = map[string]TypeConfig{
	peeringdb.TypeOrg: {
		Name: peeringdb.TypeOrg,
		Fields: map[string]FieldType{
			"id":        FieldInt,
			"name":      FieldString,
			"aka":       FieldString,
			"name_long": FieldString,
			"website":   FieldString,
			"notes":     FieldString,
			"logo":      FieldString,
			"address1":  FieldString,
			"address2":  FieldString,
			"city":      FieldString,
			"state":     FieldString,
			"country":   FieldString,
			"zipcode":   FieldString,
			"suite":     FieldString,
			"floor":     FieldString,
			"latitude":  FieldFloat,
			"longitude": FieldFloat,
			"created":   FieldTime,
			"updated":   FieldTime,
		},
		SearchFields: []string{"name", "aka", "name_long"},
		FoldedFields: map[string]bool{"name": true, "aka": true, "city": true},
	},
	peeringdb.TypeNet: {
		Name: peeringdb.TypeNet,
		Fields: map[string]FieldType{
			"id":                           FieldInt,
			"org_id":                       FieldInt,
			"name":                         FieldString,
			"aka":                          FieldString,
			"name_long":                    FieldString,
			"website":                      FieldString,
			"asn":                          FieldInt,
			"looking_glass":                FieldString,
			"route_server":                 FieldString,
			"irr_as_set":                   FieldString,
			"info_type":                    FieldString,
			"info_prefixes4":               FieldInt,
			"info_prefixes6":               FieldInt,
			"info_traffic":                 FieldString,
			"info_ratio":                   FieldString,
			"info_scope":                   FieldString,
			"info_unicast":                 FieldBool,
			"info_multicast":               FieldBool,
			"info_ipv6":                    FieldBool,
			"info_never_via_route_servers": FieldBool,
			"notes":                        FieldString,
			"policy_url":                   FieldString,
			"policy_general":               FieldString,
			"policy_locations":             FieldString,
			"policy_ratio":                 FieldBool,
			"policy_contracts":             FieldString,
			"allow_ixp_update":             FieldBool,
			"status_dashboard":             FieldString,
			"rir_status":                   FieldString,
			"rir_status_updated":           FieldTime,
			"logo":                         FieldString,
			"ix_count":                     FieldInt,
			"fac_count":                    FieldInt,
			"netixlan_updated":             FieldTime,
			"netfac_updated":               FieldTime,
			"poc_updated":                  FieldTime,
			"created":                      FieldTime,
			"updated":                      FieldTime,
		},
		SearchFields: []string{"name", "aka", "name_long", "irr_as_set"},
		FoldedFields: map[string]bool{"name": true, "aka": true, "name_long": true},
	},
	peeringdb.TypeFac: {
		Name: peeringdb.TypeFac,
		Fields: map[string]FieldType{
			"id":                          FieldInt,
			"org_id":                      FieldInt,
			"org_name":                    FieldString,
			"campus_id":                   FieldInt,
			"name":                        FieldString,
			"aka":                         FieldString,
			"name_long":                   FieldString,
			"website":                     FieldString,
			"clli":                        FieldString,
			"rencode":                     FieldString,
			"npanxx":                      FieldString,
			"tech_email":                  FieldString,
			"tech_phone":                  FieldString,
			"sales_email":                 FieldString,
			"sales_phone":                 FieldString,
			"property":                    FieldString,
			"diverse_serving_substations": FieldBool,
			"notes":                       FieldString,
			"region_continent":            FieldString,
			"status_dashboard":            FieldString,
			"logo":                        FieldString,
			"net_count":                   FieldInt,
			"ix_count":                    FieldInt,
			"carrier_count":               FieldInt,
			"address1":                    FieldString,
			"address2":                    FieldString,
			"city":                        FieldString,
			"state":                       FieldString,
			"country":                     FieldString,
			"zipcode":                     FieldString,
			"suite":                       FieldString,
			"floor":                       FieldString,
			"latitude":                    FieldFloat,
			"longitude":                   FieldFloat,
			"created":                     FieldTime,
			"updated":                     FieldTime,
		},
		SearchFields: []string{"name", "aka", "name_long", "city", "country"},
		FoldedFields: map[string]bool{"name": true, "aka": true, "city": true},
	},
	peeringdb.TypeIX: {
		Name: peeringdb.TypeIX,
		Fields: map[string]FieldType{
			"id":                        FieldInt,
			"org_id":                    FieldInt,
			"name":                      FieldString,
			"aka":                       FieldString,
			"name_long":                 FieldString,
			"city":                      FieldString,
			"country":                   FieldString,
			"region_continent":          FieldString,
			"media":                     FieldString,
			"notes":                     FieldString,
			"proto_unicast":             FieldBool,
			"proto_multicast":           FieldBool,
			"proto_ipv6":                FieldBool,
			"website":                   FieldString,
			"url_stats":                 FieldString,
			"tech_email":                FieldString,
			"tech_phone":                FieldString,
			"policy_email":              FieldString,
			"policy_phone":              FieldString,
			"sales_email":               FieldString,
			"sales_phone":               FieldString,
			"net_count":                 FieldInt,
			"fac_count":                 FieldInt,
			"ixf_net_count":             FieldInt,
			"ixf_last_import":           FieldTime,
			"ixf_import_request":        FieldString,
			"ixf_import_request_status": FieldString,
			"service_level":             FieldString,
			"terms":                     FieldString,
			"status_dashboard":          FieldString,
			"logo":                      FieldString,
			"created":                   FieldTime,
			"updated":                   FieldTime,
		},
		SearchFields: []string{"name", "aka", "name_long", "city", "country"},
		FoldedFields: map[string]bool{"name": true, "aka": true, "name_long": true, "city": true},
	},
	peeringdb.TypePoc: {
		Name: peeringdb.TypePoc,
		Fields: map[string]FieldType{
			"id":      FieldInt,
			"net_id":  FieldInt,
			"role":    FieldString,
			"visible": FieldString,
			"name":    FieldString,
			"phone":   FieldString,
			"email":   FieldString,
			"url":     FieldString,
			"created": FieldTime,
			"updated": FieldTime,
		},
		SearchFields: []string{"name", "email"},
	},
	peeringdb.TypeIXLan: {
		Name: peeringdb.TypeIXLan,
		Fields: map[string]FieldType{
			"id":                              FieldInt,
			"ix_id":                           FieldInt,
			"name":                            FieldString,
			"descr":                           FieldString,
			"mtu":                             FieldInt,
			"dot1q_support":                   FieldBool,
			"rs_asn":                          FieldInt,
			"arp_sponge":                      FieldString,
			"ixf_ixp_member_list_url_visible": FieldString,
			"ixf_ixp_import_enabled":          FieldBool,
			"created":                         FieldTime,
			"updated":                         FieldTime,
		},
		SearchFields: []string{"name", "descr"},
	},
	peeringdb.TypeIXPfx: {
		Name: peeringdb.TypeIXPfx,
		Fields: map[string]FieldType{
			"id":       FieldInt,
			"ixlan_id": FieldInt,
			"protocol": FieldString,
			"prefix":   FieldString,
			"in_dfz":   FieldBool,
			"created":  FieldTime,
			"updated":  FieldTime,
		},
		SearchFields: []string{"prefix"},
	},
	peeringdb.TypeNetIXLan: {
		Name: peeringdb.TypeNetIXLan,
		Fields: map[string]FieldType{
			"id":          FieldInt,
			"net_id":      FieldInt,
			"ix_id":       FieldInt,
			"ixlan_id":    FieldInt,
			"name":        FieldString,
			"notes":       FieldString,
			"speed":       FieldInt,
			"asn":         FieldInt,
			"ipaddr4":     FieldString,
			"ipaddr6":     FieldString,
			"is_rs_peer":  FieldBool,
			"bfd_support": FieldBool,
			"operational": FieldBool,
			"net_side_id": FieldInt,
			"ix_side_id":  FieldInt,
			"created":     FieldTime,
			"updated":     FieldTime,
		},
		SearchFields: []string{"name"},
	},
	peeringdb.TypeNetFac: {
		Name: peeringdb.TypeNetFac,
		Fields: map[string]FieldType{
			"id":        FieldInt,
			"net_id":    FieldInt,
			"fac_id":    FieldInt,
			"name":      FieldString,
			"city":      FieldString,
			"country":   FieldString,
			"local_asn": FieldInt,
			"created":   FieldTime,
			"updated":   FieldTime,
		},
		SearchFields: []string{"name"},
	},
	peeringdb.TypeIXFac: {
		Name: peeringdb.TypeIXFac,
		Fields: map[string]FieldType{
			"id":      FieldInt,
			"ix_id":   FieldInt,
			"fac_id":  FieldInt,
			"name":    FieldString,
			"city":    FieldString,
			"country": FieldString,
			"created": FieldTime,
			"updated": FieldTime,
		},
		SearchFields: []string{"name"},
	},
	peeringdb.TypeCarrier: {
		Name: peeringdb.TypeCarrier,
		Fields: map[string]FieldType{
			"id":        FieldInt,
			"org_id":    FieldInt,
			"org_name":  FieldString,
			"name":      FieldString,
			"aka":       FieldString,
			"name_long": FieldString,
			"website":   FieldString,
			"notes":     FieldString,
			"fac_count": FieldInt,
			"logo":      FieldString,
			"created":   FieldTime,
			"updated":   FieldTime,
		},
		SearchFields: []string{"name", "aka", "name_long"},
		FoldedFields: map[string]bool{"name": true, "aka": true},
	},
	peeringdb.TypeCarrierFac: {
		Name: peeringdb.TypeCarrierFac,
		Fields: map[string]FieldType{
			"id":         FieldInt,
			"carrier_id": FieldInt,
			"fac_id":     FieldInt,
			"name":       FieldString,
			"created":    FieldTime,
			"updated":    FieldTime,
		},
		SearchFields: []string{"name"},
	},
	peeringdb.TypeCampus: {
		Name: peeringdb.TypeCampus,
		Fields: map[string]FieldType{
			"id":        FieldInt,
			"org_id":    FieldInt,
			"org_name":  FieldString,
			"name":      FieldString,
			"name_long": FieldString,
			"aka":       FieldString,
			"website":   FieldString,
			"notes":     FieldString,
			"country":   FieldString,
			"city":      FieldString,
			"zipcode":   FieldString,
			"state":     FieldString,
			"logo":      FieldString,
			"created":   FieldTime,
			"updated":   FieldTime,
		},
		SearchFields: []string{"name"},
		FoldedFields: map[string]bool{"name": true},
	},
}

Registry maps PeeringDB type name strings to their TypeConfig. List and Get functions are nil until serializers are wired up.

Functions

func ParseFilters

func ParseFilters(params url.Values, tc TypeConfig) ([]func(*sql.Selector), bool, error)

ParseFilters translates Django-style query parameters into ent sql.Selector predicates. Reserved parameters (limit, skip, etc.) are skipped. Unknown fields are silently ignored per D-20 + Phase 70 D-05 / TRAVERSAL-04.

Calls ParseFiltersCtx with context.Background — unknown fields are discarded rather than surfaced. Production handlers MUST use ParseFiltersCtx with a ctx from WithUnknownFields to emit diagnostics.

Return values:

  • preds: the predicate slice to pass to ent as Where arguments
  • emptyResult: true when an __in filter was empty (?asn__in=); the caller MUST short-circuit the whole request and emit an empty data array without running SQL (Phase 69 D-06, IN-02)
  • err: set only for known fields with invalid values / operators

func ParseFiltersCtx added in v1.16.0

func ParseFiltersCtx(ctx context.Context, params url.Values, tc TypeConfig) ([]func(*sql.Selector), bool, error)

ParseFiltersCtx is the context-aware filter parser introduced by Phase 70 D-05. Unknown filter fields (including over-cap traversal keys per D-04) are silently ignored for the HTTP response AND appended to the ctx-attached accumulator so operators can observe them via slog.DebugContext + OTel.

Traversal resolution order (1-hop and 2-hop, len(relSegs) <= 2):

  1. Path A: Allowlists[tc.Name].Direct or .Via exact match
  2. Path B: LookupEdge + TargetFields introspection

Keys with len(relSegs) > 2 are silently rejected per D-04.

Phase 68 (status matrix) and Phase 69 (_fold routing, empty __in) invariants are preserved: traversal predicates wrap around buildPredicate which still consults FoldedFields on the target TypeConfig, and the empty-__in emptyResult sentinel bubbles back up from subquery construction.

func ParsePaginationParams

func ParsePaginationParams(params url.Values) (limit, skip int)

ParsePaginationParams extracts limit and skip from query parameters with defaults and bounds per D-16, D-21.

Bare URL (no `limit=`): returns DefaultLimit (0 = unlimited), matching upstream `rest.py:495` which defaults `limit` to 0 and then `rest.py:737` which slices `qset[skip:]` (no upper bound). All-rows responses are gated by the Phase 71 response-memory budget; if the precount × TypicalRowBytes exceeds the budget, the handler returns 413 application/problem+json before materialising anything.

Explicit `limit=N`: positive N is honoured, clamped to MaxLimit (1000) per D-21. limit=0 is the explicit "unlimited" sentinel and is passed through unchanged; the list closures' `if opts.Limit > 0 { .Limit(...) }` gate omits the SQL LIMIT clause when limit is 0.

Negative values are ignored (treated as missing) per upstream behaviour.

func ParseSinceParam

func ParseSinceParam(params url.Values) (*time.Time, error)

ParseSinceParam parses the ?since= query parameter as a Unix timestamp per D-15. Returns nil if the parameter is absent or empty.

func StreamListResponse added in v1.16.0

func StreamListResponse(ctx context.Context, w http.ResponseWriter, meta any, rowsIter RowsIter) error

StreamListResponse writes a PeeringDB envelope {"meta":...,"data":[...]} token-by-token without materialising the full result slice, per Phase 71 CONTEXT.md D-01.

Write sequence:

  1. Set Content-Type: application/json and X-Powered-By headers.
  2. Write `{"meta":` + json.Marshal(meta) + `,"data":[`.
  3. For each row yielded by rowsIter: emit a leading `,` (when not first), then json.Marshal(row) written directly to w.
  4. Every FlushEvery rows, flush via http.Flusher (if w implements it).
  5. Write `]}` and do a final flush.

Error handling:

  • json.Marshal(meta) failure returns the wrapped error BEFORE any bytes hit the wire, so callers can still emit a 500/problem-detail.
  • Any failure after the prelude has been written returns a wrapped error; previously-written bytes stay on the wire (the response is already committed). The caller is expected to log and drop the connection.
  • Absence of http.Flusher on w is tolerated: flushes are silently skipped.

ctx is reserved for future per-row cancellation (honor ctx.Done() inside the loop for backpressure when a client disconnects); today the iterator is driven by ent.All() or a keyset-chunked query that already respects ctx at its own boundary, so threading ctx.Err() per row is redundant.

func TargetFields added in v1.16.0

func TargetFields(targetType string) map[string]FieldType

TargetFields returns the filterable field set on the target entity of an edge lookup. Parser uses this to validate the <field> portion of <fk>__<field> before translating to a predicate in Plan 70-05.

Consults Registry[targetType].Fields — the existing TypeConfig map. Nil-safe: returns nil for an unknown target.

func TypicalRowBytes added in v1.16.0

func TypicalRowBytes(entity string, depth int) int

TypicalRowBytes returns the conservative estimated serialized size per row for the named entity at the given depth. depth is clamped to {0, 2} — the only depths pdbcompat actually serves. Unknown entities return defaultRowSize to fail-closed against surprise types (e.g. a new type added to Registry before this map is updated).

func UnknownFieldsFromCtx added in v1.16.0

func UnknownFieldsFromCtx(ctx context.Context) []string

UnknownFieldsFromCtx returns the current accumulator (possibly nil when no accumulator was attached). Callers emit slog.DebugContext + OTel span attribute from the returned slice after ParseFiltersCtx returns.

func WithUnknownFields added in v1.16.0

func WithUnknownFields(ctx context.Context) context.Context

WithUnknownFields returns a new context carrying an empty unknown-fields accumulator. Handler creates this before calling ParseFiltersCtx; the parser appends to the accumulator as it encounters unknown keys.

Ctx threading (rather than a return slice) keeps ParseFilters' existing return signature stable and avoids churning every call site that passes params straight through to ent (Phase 70 D-05).

func WriteBudgetProblem added in v1.16.0

func WriteBudgetProblem(w http.ResponseWriter, instance string, info BudgetExceeded)

WriteBudgetProblem writes the RFC 9457 413 response described by Phase 71 D-04. Does not set Retry-After — the failure is request-shape (wrong filters / too-large page), not transient resource pressure; retrying the same request would produce the same 413. Operators who want to retrieve more rows must narrow their filters or page smaller.

`instance` is written through untouched to the `instance` field of the problem-detail body when non-empty; callers typically pass r.URL.Path.

func WriteProblem added in v1.9.0

func WriteProblem(w http.ResponseWriter, input httperr.WriteProblemInput)

WriteProblem writes an RFC 9457 problem detail error response with the X-Powered-By header. This replaces the former PeeringDB error envelope with a standards-based format per ARCH-01.

func WriteResponse

func WriteResponse(w http.ResponseWriter, data any)

WriteResponse writes a successful PeeringDB-compatible JSON response with the standard envelope format per D-04. Data must be a slice.

Types

type AllowlistEntry added in v1.16.0

type AllowlistEntry struct {
	Direct []string
	Via    map[string][]string
}

AllowlistEntry is the per-entity allowlist shape emitted into internal/pdbcompat/allowlist_gen.go by cmd/pdb-compat-allowlist. Consumed at request time by ParseFilters to decide whether a <fk>__<field> key is a valid Path A traversal.

  • Direct: single-hop filter keys (e.g. "org__name"). Codegen populates this from WithPrepareQueryAllow fields that contain exactly one "__" separator AND whose first segment matches a schema edge name.
  • Via: 2-hop filter keys grouped by first hop. For "ixlan__ix__fac_count" the map entry is {"ixlan": {"ix__fac_count"}}. Codegen populates from fields that contain exactly two "__" separators.

Fields with zero or >2 "__" separators are either direct local fields (0 separators — live in TypeConfig.Fields instead) or too deep per D-04 (>2 separators — codegen drops them with a build-time warning).

type BudgetExceeded added in v1.16.0

type BudgetExceeded struct {
	MaxRows        int    `json:"max_rows"`
	BudgetBytes    int64  `json:"budget_bytes"`
	EstimatedBytes int64  `json:"-"`
	Count          int    `json:"-"`
	Entity         string `json:"-"`
	Depth          int    `json:"-"`
}

BudgetExceeded describes a request whose estimated response size exceeds the configured PDBPLUS_RESPONSE_MEMORY_LIMIT. Populated by CheckBudget; consumed by WriteBudgetProblem (413 writer) and (in later plans) structured-log emission in the handler + OTel span attributes for the memory-budget counter.

Only MaxRows and BudgetBytes are serialized onto the wire (D-04); EstimatedBytes, Count, Entity, and Depth are internal diagnostics carried in the struct so callers can log / trace them without a second trip through TypicalRowBytes.

func CheckBudget added in v1.16.0

func CheckBudget(count int, entity string, depth int, budgetBytes int64) (BudgetExceeded, bool)

CheckBudget reports whether a request of `count` rows for `entity` at `depth` fits under `budgetBytes`. Returns (zero, true) when it does, or (populated, false) with diagnostic fields when it does not.

budgetBytes <= 0 disables the check entirely — same semantic as PDBPLUS_SYNC_MEMORY_LIMIT=0. This is the documented local-dev escape hatch and the reason Phase 68's unbounded limit=0 is safe to expose in prod (the budget is the DoS safety net).

The math: perRow = TypicalRowBytes(entity, depth); estimated = count × perRow; over budget iff estimated > budgetBytes. The estimate is conservative by construction — TypicalRowBytes is the measured mean doubled and rounded up (D-03), and count is a precise SELECT COUNT(*) against the already-filtered query, so false-positive 413s are rare and preferred over OOM.

Overflow: count is an int from SELECT COUNT(*); perRow tops out near 10 KiB in the lookup table. count × perRow comfortably fits int64 until count exceeds ~10^15, which is 10^8× the current PeeringDB fleet size. No overflow guard needed in practice.

type CountFunc added in v1.16.0

type CountFunc func(ctx context.Context, client *ent.Client, opts QueryOptions) (int, error)

CountFunc runs the predicate chain for a list query and returns the matching row count WITHOUT fetching row data. Used by Phase 71 serveList pre-flight budget check to decide whether to 413 up-front before committing to an expensive .All(ctx) fetch.

The returned count reflects what WOULD be served after Offset/Limit are applied — not the raw total. This matches the budget math that multiplies count × typicalRowBytes.

type EdgeMetadata added in v1.16.0

type EdgeMetadata struct {
	Name           string
	TargetType     string
	TraversalKey   string
	Excluded       bool
	ParentFKColumn string
	TargetTable    string
	TargetIDColumn string
	OwnFK          bool
}

EdgeMetadata describes one ent edge for Path B traversal lookup. Emitted into allowlist_gen.go Edges by cmd/pdb-compat-allowlist at `go generate` time. See Phase 70 D-02 (amended 2026-04-19: the codegen-emitted static map replaces the originally-proposed runtime client.Schema.Tables walk. Rationale: deterministic, testable, no init-order coupling, matches the v1.15 Phase 63 codegen precedent; freshness is enforced by the existing go-generate drift-check CI gate).

Parser-facing fields:

  • Name: local ent edge name (e.g. "organization", "network_facilities")
  • TargetType: PeeringDB type string of the edge target (e.g. "org")
  • TraversalKey: the <fk> token used in filter params ("?org__name=foo" → TraversalKey "org"). Equals TargetType for all edges today; kept separate for future aliasing.
  • Excluded: true when the edge has WithFilterExcludeFromTraversal. LookupEdge returns (zero, false) for excluded edges.

SQL-join-facing fields (consumed read-only by Plan 70-05's subquery construction):

  • ParentFKColumn: foreign-key column name for this edge sourced from gen.Relation.Column() at codegen time. For M2O edges (edge.From with Ref().Field()) the column lives on the parent table (networks.org_id); for O2M edges (edge.To) the column lives on the child table (pocs.net_id). The subquery shape depends on which side of the join owns the column — see OwnFK below.
  • OwnFK: true when ParentFKColumn lives on the PARENT table (M2O / O2O-from-inverse); false when it lives on the CHILD / target table (O2M / O2O-from-edge). Populated at codegen time from gen.Edge.Rel.Type (M2O → true; O2M → false). Used by buildSinglHop / buildTwoHop to decide whether to filter the parent's PK against a `SELECT child.<fk>` subquery (O2M) or the parent's FK column against a `SELECT target.<id>` subquery (M2O). Without this flag, O2M edges like net→pocs produce invalid SQL referencing a non-existent networks.net_id column — see Phase 70 REVIEW CR-01 regression.
  • TargetTable: SQL table name on the target side (e.g. "organizations").
  • TargetIDColumn: typically "id"; emitted explicitly so the codegen output is self-contained and doesn't rely on a convention.

func LookupEdge added in v1.16.0

func LookupEdge(entityType, fk string) (EdgeMetadata, bool)

LookupEdge returns the edge metadata for a single hop from entityType via the filter-param token fk (e.g. LookupEdge("net", "org") returns the Network→Organization edge).

Returns (zero, false) when:

  • entityType is not in Edges (unknown PeeringDB type)
  • no edge on entityType has TraversalKey == fk
  • the matching edge is Excluded

Plan 70-05's parser calls this after a param fails direct-field lookup and also fails Path A Allowlists lookup. When this returns false, the param is the unknown-filter-field case and goes through the silent-ignore + DEBUG log + OTel attr path per D-05.

The generated Edges map is populated at package init by Go's normal mechanism and is safe for concurrent readers with no runtime guard — the codegen path makes the map effectively immutable after init.

func ResolveEdges added in v1.16.0

func ResolveEdges(entityType string) []EdgeMetadata

ResolveEdges returns all non-excluded edges for entityType. Used in tests and by future diagnostic endpoints; not on the request-time hot path. Returns nil (not a zero-length slice) for an unknown entityType so callers can distinguish "no such entity" from "entity with no traversable edges".

type FieldType

type FieldType int

FieldType represents the data type of a filterable field.

const (
	// FieldString indicates a string-typed field.
	FieldString FieldType = iota
	// FieldInt indicates an integer-typed field.
	FieldInt
	// FieldBool indicates a boolean-typed field.
	FieldBool
	// FieldTime indicates a time.Time-typed field.
	FieldTime
	// FieldFloat indicates a float64-typed field.
	FieldFloat
)

type FilterExcludeFromTraversalAnnotation added in v1.16.0

type FilterExcludeFromTraversalAnnotation = schemaannot.FilterExcludeFromTraversalAnnotation

FilterExcludeFromTraversalAnnotation aliases schemaannot.FilterExcludeFromTraversalAnnotation.

func WithFilterExcludeFromTraversal added in v1.16.0

func WithFilterExcludeFromTraversal() FilterExcludeFromTraversalAnnotation

WithFilterExcludeFromTraversal is the pdbcompat-package-level constructor for FILTER_EXCLUDE annotations (Phase 70 D-03). Re-exports schemaannot.WithFilterExcludeFromTraversal.

Initial expected call sites (Plan 70-03):

  • Any edge that materialises as a JSON field (none today — placeholder)
  • Relay id-alias edges generated by entgql (covered via codegen introspection rather than explicit annotation; the tool detects them by name).

type GetFunc

type GetFunc func(ctx context.Context, client *ent.Client, id int, depth int) (any, error)

GetFunc queries a single entity by ID and returns its serialized form.

type Handler

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

Handler serves PeeringDB-compatible API endpoints.

func NewHandler

func NewHandler(client *ent.Client, responseMemoryLimit int64) *Handler

NewHandler creates a Handler for PeeringDB-compatible API endpoints. responseMemoryLimit is the per-response byte budget consumed by the pre-flight CheckBudget gate (Phase 71 D-02). Pass 0 to disable the budget check (local dev / tests only; operators ship a non-zero PDBPLUS_RESPONSE_MEMORY_LIMIT in prod — default 128 MiB per D-05).

func (*Handler) Register

func (h *Handler) Register(mux *http.ServeMux)

Register sets up PeeringDB-compatible routes on the given mux. Routes follow PeeringDB's URL patterns: /api/{type}, /api/{type}/{id}. Both with and without trailing slash variants are handled per D-02. The index endpoint at /api/ lists all available types per D-17.

type ListFunc

type ListFunc func(ctx context.Context, client *ent.Client, opts QueryOptions) ([]any, int, error)

ListFunc queries entities and returns serialized objects plus total count.

type PrepareQueryAllowAnnotation added in v1.16.0

type PrepareQueryAllowAnnotation = schemaannot.PrepareQueryAllowAnnotation

PrepareQueryAllowAnnotation aliases schemaannot.PrepareQueryAllowAnnotation. See the schemaannot package for full docs.

func WithPrepareQueryAllow added in v1.16.0

func WithPrepareQueryAllow(fields ...string) PrepareQueryAllowAnnotation

WithPrepareQueryAllow is the pdbcompat-package-level constructor for Path A allowlist annotations (Phase 70 D-01). Re-exports schemaannot.WithPrepareQueryAllow so pdbcompat's existing API surface is unchanged. New ent/schema call sites should import the schemaannot sub-package directly to avoid the import cycle documented above.

type QueryOptions

type QueryOptions struct {
	Filters []func(*sql.Selector)
	Limit   int
	Skip    int
	Since   *time.Time
	Search  string   // ?q= parameter
	Fields  []string // ?fields= parameter
	Depth   int      // depth parameter (only used on detail)

	// EmptyResult is set by ParseFilters when the request contains an
	// __in filter with zero values (e.g. ?asn__in=). Each list closure in
	// registry_funcs.go short-circuits on this flag and returns an empty
	// result set without issuing any SQL — matches Django ORM
	// Model.objects.filter(id__in=[]) per Phase 69 D-06 (IN-02).
	EmptyResult bool
}

QueryOptions holds parsed query parameters for list endpoints.

type RowSize added in v1.16.0

type RowSize struct {
	Depth0 int
	Depth2 int
}

RowSize holds the conservative estimated serialized size in bytes per entity type at depth=0 (list-shape) and depth=2 (expanded-shape). Values are doubled from measured means per D-03 so the budget check prefers false-positive 413s over OOM. Recalibrated every major milestone; drift >20% triggers a refresh plan.

type RowsIter added in v1.16.0

type RowsIter = func() (row any, ok bool, err error)

RowsIter is a pull-style row iterator driving StreamListResponse.

Semantics:

  • (row, true, nil) — row is the next payload element; keep iterating.
  • (nil, false, nil) — clean EOF; end of stream.
  • (nil, false, err) — abort; err is returned to the caller wrapped with the failing row index.

A "zombie" iterator that returns ok=true forever is caller-unsafe; the upstream memory budget (Plan 71-03) is responsible for ensuring bounded iteration length by capping the filtered row count pre-flight.

type TypeConfig

type TypeConfig struct {
	Name         string
	Fields       map[string]FieldType
	SearchFields []string
	List         ListFunc
	Count        CountFunc
	Get          GetFunc

	// FoldedFields lists the string fields on this type that have a sibling
	// <field>_fold column populated by the sync worker (Phase 69 Plan 03).
	// When non-nil, substring / prefix / iexact filters on these fields are
	// routed to the _fold column with unifold.Fold(value) on the RHS for
	// diacritic-insensitive matching (Phase 69 UNICODE-01). Nil is safe —
	// map reads on nil return the zero value (false).
	FoldedFields map[string]bool
}

TypeConfig describes a PeeringDB object type for the compatibility layer.

Directories

Path Synopsis
Package parity holds regression tests that lock v1.16 pdbcompat semantics against future drift.
Package parity holds regression tests that lock v1.16 pdbcompat semantics against future drift.
Package schemaannot exposes ent schema annotation types consumed by ent/schema/*.go to describe pdbcompat's Path A allowlist (Phase 70 D-01) and per-edge FILTER_EXCLUDE (D-03).
Package schemaannot exposes ent schema annotation types consumed by ent/schema/*.go to describe pdbcompat's Path A allowlist (Phase 70 D-01) and per-edge FILTER_EXCLUDE (D-03).

Jump to

Keyboard shortcuts

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