feat(promote): resolve pre-existing entities by business key (#entity:field=value) #24

Merged
arcodange merged 1 commits from claude/dolibarr-promote-lookup into main 2026-06-29 23:58:42 +02:00
Owner

Closes the last promote gap: a manifest can now reference records it does not create.

A value like #thirdparty:name=KissMetrics (or :code=CL0007) is looked up on the target at apply time and resolved to that target's id — so the same manifest is portable (sandbox id on --target sandbox, prod id on --target prod). Combined with @ref (created entities), arbitrary change-sets replay cleanly.

  • promote-apply.sh resolve() gains a # branch + a lookup() helper querying the target via the GET wrapper with sqlfilters. Supports thirdparty (name/code/supplier_code) and invoice/supplierinvoice (ref/ref_supplier).
  • Safety: a lookup matching nothing or more than one record aborts the run — it never guesses, so it can't write to the wrong entity.

Proven live

#thirdparty:name=ACME Conseil  → resolved to the existing client → invoiced it (id 25) + paid
#thirdparty:code=NOPE9999      → 404, run aborted (exit 1)
#thirdparty:name=ZZZ-DUP (2x)  → "ambiguous (2 matches)", run aborted (exit 1)

So "invoice the existing client KissMetrics and record its payment" is now expressible and portable across sandbox→prod. That completes the promote flow.

🤖 Generated with Claude Code

Closes the last promote gap: a manifest can now reference records it does **not** create. A value like **`#thirdparty:name=KissMetrics`** (or `:code=CL0007`) is looked up on the **target** at apply time and resolved to *that target's* id — so the same manifest is portable (sandbox id on `--target sandbox`, prod id on `--target prod`). Combined with `@ref` (created entities), arbitrary change-sets replay cleanly. - `promote-apply.sh` `resolve()` gains a `#` branch + a `lookup()` helper querying the target via the GET wrapper with `sqlfilters`. Supports `thirdparty` (name/code/supplier_code) and `invoice`/`supplierinvoice` (ref/ref_supplier). - **Safety:** a lookup matching **nothing or more than one** record **aborts the run** — it never guesses, so it can't write to the wrong entity. ### Proven live ``` #thirdparty:name=ACME Conseil → resolved to the existing client → invoiced it (id 25) + paid #thirdparty:code=NOPE9999 → 404, run aborted (exit 1) #thirdparty:name=ZZZ-DUP (2x) → "ambiguous (2 matches)", run aborted (exit 1) ``` So *"invoice the existing client KissMetrics and record its payment"* is now expressible and portable across sandbox→prod. That completes the promote flow. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
arcodange added 1 commit 2026-06-29 23:58:31 +02:00
Closes the last promote gap: a manifest can now reference records it does NOT
create. A value like "#thirdparty:name=KissMetrics" (or :code=CL0007) is looked
up on the TARGET at apply time and resolved to that target's id — so the same
manifest is portable (sandbox id on --target sandbox, prod id on --target prod).

promote-apply.sh: resolve() gains a "#" branch + a lookup() helper that queries
the target via the GET wrapper with sqlfilters. Supports thirdparty
(name/code/supplier_code) and invoice/supplierinvoice (ref/ref_supplier). A
lookup matching nothing OR more than one record ABORTS the run — it never
guesses, so it cannot write to the wrong entity.

Proven live: "#thirdparty:name=ACME Conseil" resolved to the existing client and
invoiced it; a not-found code and an ambiguous (2-match) name both aborted with
exit 1. Combined with @refs, arbitrary self-contained-or-referential change-sets
now replay cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arcodange merged commit 09a2cbab0a into main 2026-06-29 23:58:42 +02:00
arcodange deleted branch claude/dolibarr-promote-lookup 2026-06-29 23:58:42 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: arcodange-org/erp#24