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
- Variables
- func ParseFilters(params url.Values, tc TypeConfig) ([]func(*sql.Selector), bool, error)
- func ParseFiltersCtx(ctx context.Context, params url.Values, tc TypeConfig) ([]func(*sql.Selector), bool, error)
- func ParsePaginationParams(params url.Values) (limit, skip int)
- func ParseSinceParam(params url.Values) (*time.Time, error)
- func StreamListResponse(ctx context.Context, w http.ResponseWriter, meta any, rowsIter RowsIter) error
- func TargetFields(targetType string) map[string]FieldType
- func TypicalRowBytes(entity string, depth int) int
- func UnknownFieldsFromCtx(ctx context.Context) []string
- func WithUnknownFields(ctx context.Context) context.Context
- func WriteBudgetProblem(w http.ResponseWriter, instance string, info BudgetExceeded)
- func WriteProblem(w http.ResponseWriter, input httperr.WriteProblemInput)
- func WriteResponse(w http.ResponseWriter, data any)
- type AllowlistEntry
- type BudgetExceeded
- type CountFunc
- type EdgeMetadata
- type FieldType
- type FilterExcludeFromTraversalAnnotation
- type GetFunc
- type Handler
- type ListFunc
- type PrepareQueryAllowAnnotation
- type QueryOptions
- type RowSize
- type RowsIter
- type TypeConfig
Constants ¶
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 )
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.
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 ¶
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.
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).
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.
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 ¶
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):
- Path A: Allowlists[tc.Name].Direct or .Via exact match
- 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 ¶
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 ¶
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:
- Set Content-Type: application/json and X-Powered-By headers.
- Write `{"meta":` + json.Marshal(meta) + `,"data":[`.
- For each row yielded by rowsIter: emit a leading `,` (when not first), then json.Marshal(row) written directly to w.
- Every FlushEvery rows, flush via http.Flusher (if w implements it).
- 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
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
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
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
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
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
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
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 Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler serves PeeringDB-compatible API endpoints.
func NewHandler ¶
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).
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
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
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.
Source Files
¶
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). |