Skip to content

feat(brands): persist and return URL type field in brand_sites (LLMO-4058)#2149

Merged
dzehnder merged 8 commits intomainfrom
feat/LLMO-4058-persist-base-url-type
Apr 9, 2026
Merged

feat(brands): persist and return URL type field in brand_sites (LLMO-4058)#2149
dzehnder merged 8 commits intomainfrom
feat/LLMO-4058-persist-base-url-type

Conversation

@dzehnder
Copy link
Copy Markdown
Contributor

@dzehnder dzehnder commented Apr 7, 2026

Summary

  • Updates syncBrandSites() to track and persist the type field (e.g. "base") from URL entries to the brand_sites table
  • Updates mapDbBrandToV2() and BRAND_SELECT to return type on base-URL entries
  • 3 new test cases covering type persistence, type in V2 mapping, and backward compatibility

Prerequisite: DB migration required before deploy:

ALTER TABLE brand_sites ADD COLUMN type TEXT DEFAULT NULL;

Companion PR: adobe-rnd/llmo-data-retrieval-service#1222 (DRS injects baseUrl with type: "base")

Jira: LLMO-4058

Test plan

  • CI green (unit tests)
  • DB migration applied to dev/stage
  • Integration test: upsert a brand with a URL having type: "base", verify GET returns it
  • Deploy before companion DRS PR

🤖 Generated with Claude Code

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 7, 2026

This PR will trigger a minor release when merged.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

dzehnder added 2 commits April 8, 2026 16:43
…4058)

  Update brands-storage to persist the type field (e.g. \"base\") through
  syncBrandSites and return it on base-URL entries in mapDbBrandToV2.
  Companion to DRS change that injects baseUrl with type: \"base\".

  Requires DB migration: ALTER TABLE brand_sites ADD COLUMN type TEXT DEFAULT NULL;
@dzehnder dzehnder force-pushed the feat/LLMO-4058-persist-base-url-type branch from 3b1631c to 4ec3781 Compare April 8, 2026 14:45
Copy link
Copy Markdown
Contributor

@irenelagno irenelagno left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

🟡 type only emitted on root path — consider a clarifying comment (Low)

In mapDbBrandToV2:

if (p === '/' && hasText(bs.type)) entry.type = bs.type;

This is correct behavior — subpaths under a base URL shouldn't carry type: "base". But it's non-obvious to future readers why the condition is p === '/'. A short inline comment would help, e.g.:

// Only the root entry (/) carries the base-URL type; subpaths are plain URLs
if (p === '/' && hasText(bs.type)) entry.type = bs.type;

🟢 typeByBase hardcoded to "base" (Nit)

if (typeof u === 'object' && u?.type === 'base') typeByBase.set(normalizedBase, 'base');

This only tracks type === 'base'. If another URL type is ever introduced it won't be persisted. Could future-proof with:

if (typeof u === 'object' && u?.type) typeByBase.set(normalizedBase, u.type);

Not blocking — just a consideration for extensibility.


Overall the changes look solid. CI is green. 👍

dzehnder and others added 5 commits April 8, 2026 17:02
- Add clarifying comment on root-path type condition in mapDbBrandToV2
- Make typeByBase generic to store any URL type, not just "base"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@alinarublea alinarublea left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Staff/Distinguished Engineer Review

Verdict: Request changes (1 blocking, rest are suggestions)

The implementation is solid structurally. Review feedback from @irenelagno was addressed well. Test coverage is complete per Codecov. The backward-compatibility story is good (no type key emitted when absent).

What's good

  • hasText(bs.type) guard means the response shape stays clean — no type: null leaking into the API contract.
  • The clarifying comment on the root-path condition is helpful.
  • Making typeByBase generic (storing any type, not just "base") was the right call.
  • Test cases cover the three important scenarios: type present, type absent, and round-trip through upsert.

Blocking

  1. Last-write-wins data loss on typeByBase. If a caller sends two URL objects with the same base URL but different types, the last one silently overwrites the first:

    urls: [
      { value: 'https://adobe.com/en', type: 'base' },
      { value: 'https://adobe.com/fr', type: 'localized' }
    ]

    Both normalize to https://adobe.com. The map ends up with type: 'localized', dropping 'base' with no warning. This is a silent data loss bug. Either:

    • Validate that conflicting types for the same base URL are rejected (throw), or
    • Store type per-path rather than per-base, if the model supports it, or
    • Document that type is strictly per-base-URL and the first/last-one-wins behavior is intentional (with a test proving it).

Non-blocking suggestions

  1. No input validation on type values. Any string passes through to the database. If type has a defined vocabulary ('base', potentially others), validate at the API boundary. A const VALID_BRAND_SITE_TYPES = new Set(['base']) with a check is ~3 lines and prevents schema drift.

  2. type is an extremely generic column/field name. In JavaScript, type isn't reserved but it collides with TypeScript discriminators, JSON-Schema keywords, and common destructuring patterns. url_role or site_type would be more self-documenting and grep-friendly. I realize this is likely settled at the API contract level already, but raising it — renaming is cheapest before any consumers depend on it.

  3. Commit hygiene. 7 commits including 2 merge commits and a CI-retrigger empty commit. Squash-merge this to keep main history clean.

  4. Test: verify the overwrite scenario. Add a test for the case in point #1 — two URL objects with the same base URL but different types. Whatever the intended behavior is, it should be documented by a test.

Cross-PR considerations

  • Deploy ordering is correct and well-documented: migration first, then API, then DRS is already merged and will start sending type: "base".
  • Rollback story: If the API PR needs to be rolled back, the column stays in the DB harmlessly (nullable, no consumers). Clean.
  • Schema evolution: If more types are added later, the TEXT column + application-level validation approach will work, but consider the CHECK constraint (PR #326 feedback) to keep the DB as the source of truth for allowed values.

…ta loss

When multiple URLs share the same base domain with different types,
the first type now wins instead of the last silently overwriting.
Added test case for conflicting types scenario.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@alinarublea alinarublea left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Staff/Distinguished Engineer Re-review

Verdict: Approve

The blocking issue from the first review has been addressed in commit 2e47cd2:

Blocking item — resolved

Last-write-wins data loss — now uses first-write-wins with !typeByBase.has(normalizedBase) guard. The comment explains the intent clearly. A test ('uses first type when multiple URLs share same base with different types') proves the behavior. The "first wins silently" approach is reasonable for this use case — the primary caller (DRS brandalf sync) sends type: "base" only on the canonical URL, so conflicts are unlikely in practice.

Non-blocking items — status

These remain unaddressed but are not blocking:

  • Input validation on type: Partially mitigated by the CHECK constraint added in the companion migration PR (#326), which will reject anything other than 'base' at the DB level. Application-level validation would still be a nice-to-have for better error messages, but the DB constraint is the more important guard.
  • Generic type field name: Acknowledged as likely settled at the API contract level.
  • Commit hygiene: 8 commits including merge commits. Recommend squash-merge.

Overall a solid, well-tested change. The cross-PR story (migration → API → DRS) is clean and the rollback path is safe.

@dzehnder dzehnder merged commit 9e403d2 into main Apr 9, 2026
18 checks passed
@dzehnder dzehnder deleted the feat/LLMO-4058-persist-base-url-type branch April 9, 2026 17:17
solaris007 pushed a commit that referenced this pull request Apr 10, 2026
# [1.431.0](v1.430.0...v1.431.0) (2026-04-10)

### Bug Fixes

* update waitlisted PLG ([#2170](#2170)) ([264716e](264716e))

### Features

* **brands:** persist and return URL type field in brand_sites (LLMO-4058) ([#2149](#2149)) ([9e403d2](9e403d2)), closes [adobe-rnd/llmo-data-retrieval-service#1222](https://github.com/adobe-rnd/llmo-data-retrieval-service/issues/1222)
@solaris007
Copy link
Copy Markdown
Member

🎉 This PR is included in version 1.431.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

irenelagno added a commit that referenced this pull request Apr 13, 2026
The brand_sites_type_check constraint (added in #2149) only allows
'base' and 'localized'. All brand URL callsites were still passing
type: 'url', causing upsertBrand to fail silently and leaving v2
onboarded sites without an active brand — which in turn caused the
DRS brand-presence scheduler to 422.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
irenelagno added a commit that referenced this pull request Apr 13, 2026
The brand_sites_type_check constraint (added in #2149) only allows
'base' and 'localized'. All brand URL callsites were still passing
type: 'url', causing upsertBrand to fail silently and leaving v2
onboarded sites without an active brand — which in turn caused the
DRS brand-presence scheduler to 422.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants