feat(contact): one structured mailing_address replaces five flat mailing_* fields (#1836) - #1997
Merged
objectstack-fleet[bot] merged 5 commits intoOct 3, 2026
Conversation
…ing_* fields crm_contact now declares `mailing_address: Field.address()`, the shape crm_account.billing_address and crm_lead.address already use. The import mapping keeps the template's five Mailing columns and points each at a declared part of the compound (`mailing_address.street` ... `.country`), the target spelling @objectstack/spec 17.6.0's ImportFieldMappingSchema documents. The contact form section renders the one field, and each locale pack carries one label row instead of five. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
`import-mappings` asserted `target in fields`, which a compound-part target (`mailing_address.street`) can never satisfy. It now asks `unknownImportMappingTargets` from @objectstack/spec/data, the verdict the import endpoint applies before any row, so a declared part passes and an undeclared one still fails. docs/STATUS.md carries the new field count the `pnpm validate` summary prints (363 -> 359: five fields became one). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
The contacts page in all three locales now describes one structured Mailing Address instead of five separate fields. The import guides say the contact template's five Mailing columns fill the parts of that one field, and drop the claim that a structured address cannot be assembled from columns, which the contact mapping now does. The account mapping's prose and REQ-0004 no longer point at the retired `mailing_*` fields. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
… columns `scripts/backfill-contact-mailing-address.ts` reads the five retired `mailing_*` columns, which an upgrade leaves in the database as orphaned columns that an unprojected REST read still returns, and writes their non-blank values as the parts of `mailing_address`. Report-only unless `--apply`; writes only an empty `mailing_address`, never overwrites one a person set after the upgrade, never touches the old columns, and a second pass reports nothing to do. It must run before `os migrate apply --allow-destructive`, which drops orphaned columns. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Oct 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1836
Clause-②: no. This is app metadata on
crm_contactand converges on the shapecrm_accountandcrm_leadalready use; it touches no published schema and no accept set.What changes
crm_contactnow stores its mailing address as one structuredmailing_address(Field.address()), the shapecrm_account.billing_addressandcrm_lead.addressalready use. The five flat text fieldsmailing_street·mailing_city·mailing_state·mailing_postal_code·mailing_countryretire in the same change. The shipped import templateassets/import-templates/contacts.csvis unchanged: the mapping sends each of its fiveMailing …columns to one part of the compound (mailing_address.street…mailing_address.country). Existing rows are converted once byscripts/backfill-contact-mailing-address.ts.The blocker R64 measured on 17.4.0 is gone: on the 17.6.0 pin the importer writes compound parts from columns (objectstack
443b2f4f). The target spellingfield.partis read from the installed@objectstack/spec17.6.0ImportFieldMappingSchema.target("a declared part of a compound field is written field.part (e.g. mailing_address.street)").Ruling
5731202390→ evidence@objectstack/*17.6.0)Field.address()value renders as one unit on the contact detail page (#664)MAILING ADDRESS=12 Harbour Way, Austin, TX 78701, USA. Probe: no"street":and no[object Object]in the page text.mailing_address: Field.address(); the fivemailing_*fields retire in the same changesrc/sales/objects/contact.object.ts363 Fields→359 Fields. ServedGET /api/v1/meta/object/crm_contact: the only mailing field is{"mailing_address":"address"}.contacts.csvkeeps its five columns, mapped intostreet/city/state/postalCode/countrysrc/sales/mappings/contact_import.mapping.ts.contacts.csvis untouched:git diff 4807ed9e -- assets/import-templates/contacts.csvis emptyPOST /api/v1/data/crm_contact/import(mappingName: crm_contact_import), afteraccounts.csv(50/50). main4807ed9e: dryRun 50 total / 44 ok / 6 errors; commit 44 ok / 6 errors. This branch: dryRun 44 ok / 6 errors; commit 44 ok / 6 errors. The same six rows fail on both (Department: "HR" is not a known option, see notes). REST read-back:ana.haddad@…→{"street":"12 Harbour Way","city":"Austin","state":"TX","postalCode":"78701","country":"USA"};ben.ibarra@…→{"street":"480 Sequoia Ave","city":"Portland","state":"OR","postalCode":"97205","country":"USA"}. Edge rows (scratch CSV): all five cells blank →mailing_address: null; only city and country →{"city":"Lyon","country":"France"}.src/sales/views/contact.view.tspostalCode: "78702".src/sales/translations/{en,zh-CN,es-ES,ja-JP}/objects.customer.tsmailing_addressrow in each pack.pnpm lint:i18n-gate: 0i18n/missing-*.content/docs/sales/contacts{,.zh-Hans,.zh-Hant}.mdxscripts/backfill-contact-mailing-address.tscontact.object.tsin one sentencesrc/sales/objects/contact.object.tsmailing_address.Conversion (
scripts/backfill-contact-mailing-address.ts)Same shape as
backfill-line-number.ts: REST only, report-only unless--apply, read-back after writing, rerunnable.os migrate plancalls themunmapped_column; onlyos migrate apply --allow-destructivedrops them). Measured on 17.6.0 + SQLite: an unprojectedPOST …/crm_contact/querystill returnsmailing_street…mailing_country, while naming one infieldsis refused (400 INVALID_FIELD: Unknown field 'mailing_street'). The script reads the unprojected row. The changeset tells operators to run it before the destructive migrate.main's artifact (4807ed9e) on a fresh SQLite DB and imported the shipped CSVs: 53 contacts, 44 with the five columns filled. Then booted this branch's artifact on a copy of that DB. Before running the script, one contact was changed to have only city and country (blank street, state and postal code), and one hadmailing_addressset over REST to stand for an address someone entered after the upgrade.mailing_addressalready holds a different value (kept) · 0 unmappable · 43 to convert. DB afterwards:mailing_addressset only on the one pre-set row (nothing written).--apply:Converted 43/43; 0 still without their address., exit 0. Read-back: full row → all five parts; the partial row →{"city":"Portland","country":"USA"}; the pre-set row kept its own value; an empty row staysnull. The old columns are untouched (44 still filled).--apply: 43 already converted · 0 to convert ·Nothing to convert., exit 0.Ablation: a mapping target that names a part that does not exist
contact_import.mapping.tstargetmailing_address.postalCode→mailing_address.planet, done throughobjectstack/scripts/ablation-replace.mjs(WRAP mode, restore on EXIT/INT/TERM) on the committed tree.pnpm buildexit 1:✗ Author-time rules failed (1 issue)—Import mapping "crm_contact_import" writes target "mailing_address.planet", which names no field … "planet" is not a part of the address field "mailing_address": the parts a target may name on it are street, city, state, postalCode, country, countryCode, formatted.pnpm validateexit 1, same diagnostic.test/import-mappings.test.tsred: 1 failed / 38 passed, offenderfieldMapping[12].target "mailing_address.planet" (unknown).dist/never got the mutation (marker count 0), and the boot inside that run served the earlier good artifact (44/6). That leg does not count. The import dry run was then measured on a copy of the built artifact with the same one-target edit (marker count 1 in the copy, 0 in the repodist/), on a fresh boot: dryRun 400INVALID_FIELDUnknown field 'mailing_address.planet' on object 'crm_contact' … the import is refused before any row, on the dry run and the commit alike. The commit gave the same 400. Contacts after: 9 (seed only, nothing imported).blob after restore ce2e9ea3274d == blob at HEAD ce2e9ea3274d,git diff HEADempty (tool verdict).git statusclean afterwards.Verification
OS_VERIFY_LOCK_SLOT=hotcrm-issue-1836 bash …/os-verify-lock.sh -c 'pnpm verify'at85362cf0:os-verify-lock: VERDICT command-exit 0. Inside it:✓ Validation passed;tsc --noEmitclean (--listFilesincludes the new script and the edited test); lint1 warning(s), 18 suggestion(s)(none oncrm_contact); i18n gate 0;✓ source hygiene clean;✓ source token ratchet clean;✓ Build complete;Test Files 176 passed (176)·Tests 3787 passed | 1 skipped (3788).src/sales, before (4807ed9e) → after: business semantics ~56,505 → ~56,418; interaction layer ~27,996 → ~27,968; authored total ~99,987 → ~99,882. No ceiling changed.objectstack.config.tsand an explicit--artifact, andGET /api/v1/meta/object/crm_contactand…/mapping/crm_contact_importreturned the converted field and the part targets. The browser-checked artifact is the85362cf0build (md5ab0dc148…).Files outside the claim's file surface, and why
docs/STATUS.md— thepnpm validatesummary figure (363 Fields→359 Fields).test/docs-declared-versions.test.tspins it.content/docs/guides/importing-your-data{,.zh-Hans,.zh-Hant}.mdxandimport-and-export{,.zh-Hans,.zh-Hant}.mdx— said contacts have separate address fields and that a structured address "cannot be assembled from separate spreadsheet columns". This change makes both false.src/sales/mappings/account_import.mapping.ts(comment only) — its last sentence relied on the contact'smailing_*text fields, and its "cannot compose" claim is contradicted by the contact mapping now.docs/requirements/0004-contact-buying-centre-map.md— named "the structuredmailing_*block" (the R64 report's carrier note).test/import-mappings.test.ts— the affected test. It now asks the platform'sunknownImportMappingTargets(@objectstack/spec/data) instead oftarget in fields.Acceptance notes
mainand on this branch alike:Department: "HR" is not a known option(rows 7, 15, 23, 31, 39, 47; the option is valuehr, labelHuman Resources). Not touched here. Reported to the seat.scripts/backfill-line-number.tsandscripts/backfill-owner-id.tsfail on the 17.6.0 pin at their first query. Measured on a fresh boot:Backfill failed: query crm_opportunity_line_item → 400: Invalid query requestandBackfill failed: cannot read sys_user (400). 17.6.0's query schema refusesfilters: [](query.filters min_items) and a stringsort: 'id asc'(query.sort invalid_shape). The new script uses neither. Reported to the seat, not fixed here.test/import-mappings.test.ts(point 1) still says a target naming no field is "not an error at import time". The ablation above measures the opposite on 17.6.0 (build, validate and the endpoint all refuse). Left as is because it is outside this card. Will be picked up by whoever next edits it.Generated by Claude Code