feat(aao-directory): wire ?include=properties server implementation#4927
Conversation
…4890) Schema and docs already on main (4890-aao-include-properties.md changeset). This commit wires the server side: - DB: AgentPublisherDetailRow gains property_ids; getPublishersForAgentDetail accepts includePropertyIds; SQL adds CASE WHEN short-circuit ARRAY_AGG. Also makes properties_authorized symmetric with ARRAY_AGG by filtering dp.property_id IS NOT NULL so len(property_ids) == properties_authorized. - Service: passes includePropertyIds through. - Route: parses ?include (repeated-key, comma-rejected 400, unknown 400); serializes property_ids when set; updates ETag; updates Zod + OpenAPI. - Tests: DB-level (sorted IDs, empty set, default null) and HTTP-level (?include=properties, empty [], unknown 400, comma 400, ETag difference). https://claude.ai/code/session_012j248bSRq5CcDQEwuberu1
…lude-properties-server-impl
There was a problem hiding this comment.
Server wiring is clean — ?include parser is symmetric with ?status, ETag composition is right, OpenAPI shape matches the wire contract. One silent semantics change to properties_authorized slips through under the "invariant fix" framing — flagging for follow-up, not blocking.
Things I checked
?includeparser atserver/src/routes/registry-api.ts:7197-7220: repeated-key form, comma-separated rejected 400, unknown values rejected 400. Same encoding as?status. Nested-objectParsedQs(?include[foo]=bar) falls through to generic 400. Right shape.- SQL change at
server/src/db/federated-index-db.ts:373-385:CASE WHEN $6::boolean THEN (SELECT ARRAY_AGG(DISTINCT dp.property_id ORDER BY dp.property_id) ...) ELSE NULL END. Postgres short-circuits when false — zero cost on default calls. Sorted aggregate keeps the ETag stable. - ETag at
server/src/routes/registry-api.ts:7342-7347: coversincludeflag and per-row IDs. Different response shapes hash differently. Required for cache correctness. - Empty-set wire semantics: DB returns
nullfor emptyARRAY_AGG(Postgres native), route coerces to[]viar.property_ids ?? []. Absent-vs-[]-vs-populated three-state matchesstatic/schemas/source/aao/agent-publishers.json:39-46, 78-81. Mirrors OpenRTB opt-in extension convention (omit-when-not-requested). - OpenAPI deltas at
static/openapi/registry.yaml:3555-3565, 3615-3619, 3707-3717, 3767-3771on both/v1and/api/v1paths. - Tests: HTTP-level covers surfaces, default omit, empty
[], unknown 400, comma 400, ETag differs. DB-level covers sorted IDs, empty null, default null. - Changeset: empty frontmatter is the project convention for server-only follow-up PRs after a spec changeset (
4890-aao-include-properties.mdalready shipped the protocol bump). No double-bump needed for the additive surface.
Follow-ups (non-blocking — file as issues)
-
properties_authorizedcount semantics narrowed silently.server/src/db/federated-index-db.ts:362-373flipped fromCOUNT(DISTINCT apa.property_id)(FK todiscovered_properties.id) toCOUNT(DISTINCT dp.property_id)filtered todp.property_id IS NOT NULL(canonical slug). For any authorized property without a canonical slug, the count drops by 1 — including on default calls with?include=propertiesnot set. The PR claims this enforces a "spec MUST" oflen(property_ids) == properties_authorized; neitherstatic/schemas/source/aao/agent-publishers.jsonnordocs/aao/directory-api.mdx:164carries that as normative MUST language (both describe the relationship descriptively). Two paths: (a) land the MUST in spec text in a follow-up so the count change is a documented correction — preferred, slug-less properties are un-addressable downstream anyway; or (b) keep the old count on default calls and only narrow when?include=propertiesis set. Either way the changeset prose should call out the default-call behavior change; current framing reads as purely additive. -
ETag input delimiter-collides on operator-controlled IDs.
server/src/routes/registry-api.ts:7347joinsr.property_idswith,inside a|-delimited row string.property_idis TEXT with no shape constraint.[\"a,b\",\"c\"]and[\"a\",\"b,c\"]hash to the same ETag. Theoretical for now (operators control their own slugs) but the surrounding code already runs everything throughJSON.stringify— push the array in directly instead of pre-joining and the JSON encoder handles escaping. -
Test gap:
?include=properties×status=revokedinteraction. The SQL surfacesproperty_idsindependent of revocation state, which is the right call (set-diff against a revoked publisher is the use case). No integration test covers it. -
Test gap: ETag changes when set membership changes but count stays the same. The motivating "count-equality is not set-equality" line deserves a direct test — seed two properties, swap one for another with the same count, assert different ETags.
Minor nits (non-blocking)
- OpenAPI description duplicated.
static/openapi/registry.yaml:3555-3565and3707-3717carry the same description string inside theschema:block and again at the parameter level. Functionally fine, redundant.
Approving on the strength of the wire shape, the empty-changeset convention, and the test coverage. A "fix" that hinges on a MUST nobody can locate in-tree is a notable framing — worth tightening before the count change ships to production dashboards.
|
The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final This is an automated message from the Argus AI review workflow. |
Closes #4890
Schema and docs for
?include=propertiesare already onmain(merged via #4894, spec-only changeset4890-aao-include-properties.md). This PR wires the server side so the endpoint actually parses the flag and serializesproperty_ids[].What changed
federated-index-db.ts—AgentPublisherDetailRowgainsproperty_ids: string[] | null;getPublishersForAgentDetailacceptsincludePropertyIds?: boolean; SQL adds aCASE WHEN $6 THEN ARRAY_AGG(dp.property_id ORDER BY dp.property_id) ELSE NULL ENDsubquery that PostgreSQL short-circuits when false (zero cost on default calls). Also fixesproperties_authorizedto filterdp.property_id IS NOT NULL, making it symmetric with theARRAY_AGGsubquery solen(property_ids) == properties_authorizedholds (the spec's MUST invariant — without the fix, properties without a canonical string ID would be counted but not named).federated-index.ts— passesincludePropertyIdsthrough to the DB layer.registry-api.ts— parses?include(repeated-key form, same encoding as?status; comma-separated form rejected 400; unknown values rejected 400); passes flag to service; spreadsproperty_ids: r.property_ids ?? []onto each shaped row when set (SQLNULLfrom empty ARRAY_AGG coerces to[]— correct: empty array signals "flag was honored, zero results" vs absent field which signals "flag not set"); updates ETag to cover the include flag and resolved IDs; updatesAgentPublishersEntrySchemaand OpenAPI query schema.registry-agent-publishers-detail.test.ts): sorted IDs, empty-set returnsnullat DB layer, default leavesproperty_idsnull. HTTP-level (registry-api-agent-publishers.test.ts):?include=propertiessurfaces IDs, default omits field, empty-authorized returns[], unknown value 400, comma-separated 400, ETag differs with/without flag.Non-breaking justification:
?includeis a new optional query parameter; default behavior (noproperty_idsin response) is unchanged.property_idsis optional in the Zod schema. Existing clients receive an identical envelope. Theproperties_authorizedfix is a data-accuracy correction; any property missing a canonicalproperty_idslug was never reliably surfaceable anyway.Changeset:
--empty(server-side implementation; the protocol changeset4890-aao-include-properties.mdalready covers the schema/docs bump).Pre-PR review:
property_ids: []for zero-authorized case (done). Nit:?include[foo]=barsilently drops nested ParsedQs (noted, consistent with how Express handles analogous edge cases across the codebase). Nit: 404-probe omitsincludePropertyIdscorrectly (no change needed).?includerepeated-key form mirrors?statusexactly. SortedARRAY_AGGis correct (deterministic ETag). Blocker fixed:properties_authorizednow filtersdp.property_id IS NOT NULL, making it symmetric withARRAY_AGGso theMUST len(property_ids) == properties_authorizedinvariant holds.r.property_ids ?? []is correct wire form per OpenRTB pattern (empty array distinct from absent field).Session: https://claude.ai/code/session_012j248bSRq5CcDQEwuberu1
Generated by Claude Code