Migrate Worker-stored allocation keys to the JVM entity shape
AI-generated content
This document was generated by an AI assistant. Verify accuracy before relying on the details.
Until lot 62 (gaps.md § AK-1008-1) services/utilities stored the « Ajouter une clé de répartition » request body as it came, so adb-utilities.allocationkeys holds two shapes: the JVM's AllocationKeyEntity (catalog ids, BigDecimal text, ISODates) and raw Worker bodies (catalog objects, numbers, date strings, the dialog's sub-tree and display members). The legacy rows make the JVM list of their part answer 400 (No enum constant CatalogValueCode) and the Worker's $year search answer 500. Since lot 62 the Worker reads both and writes the entity. tools/scripts/migrate-allocation-key-shape.mjs rewrites the legacy rows with lot 62's own write mapper (allocationKeyToEntity, imported, not copied) and never touches a JVM-shaped row (gaps.md § AK-1008-2). Dry-run by default, refuses a production-looking cluster, one guarded replaceOne per key, idempotent, write-ahead rollback log.
At a glance
| Fact | Value |
|---|---|
| Script | tools/scripts/migrate-allocation-key-shape.mjs (core + tests in tools/scripts/lib / tools/scripts/test) |
| Runs as | an engineer, from a workstation — never from a Worker or CI |
| Reads | every document of adb-utilities.allocationkeys, _id order, promoteValues: false (each number keeps its BSON type) |
| Selects | in process, by shape: a key with at least one legacy trait (below). A key with none is never written |
| Converts with | allocationKeyToEntity of services/utilities/src/allocationKeyDto.mjs — what the Worker's POST / PUT store — under the key's own _id |
| Checks each rewrite | no legacy trait left, _class = AllocationKeyEntity, and the API DTO (allocationKeyToDto) the same before and after |
| Writes | replaceOne, guarded by _id, every top-level member as read and _class absent. No insert, delete, index or transaction |
| Log | write-ahead JSON Lines, --log <path> (default ./migrate-allocation-key-shape-<timestamp>.jsonl) |
| Rollback | --rollback <log> (dry-run) then --rollback <log> --apply — restores the stored bytes, int32 included |
| Production guard | refused unless --i-know-this-is-prod; a host is non-production only if local or named like one |
| Side effects | none known: no Atlas trigger and no services/event-dispatch route watch allocationkeys |
| Staging, 2026-10-08 (dry-run) | 44 documents, 21 JVM-shaped, 23 legacy (5 traits each), 23 to rewrite, 0 reported |
Details
The legacy traits
| Trait | Legacy (Worker body) | JVM entity |
|---|---|---|
catalog-object | total.unit / shares[].quantity.unit = {id, code, value, label} | "QUANTITY.KW_PER_HOUR" |
number-amount | an int32 / double amount | the BigDecimal's text, "0" |
string-date | interval.start / .end = "2026-10-05" | an ISODate at UTC midnight |
nested-shares | each share carries the dialog's shares sub-tree (empty on staging) | absent — one rolled-up share per target |
display-fields | type discriminators, openDetails, and per share UnitName, UnitId, unitMortageNumber, owner | absent |
target.properties is a free map on both sides and is never a trait. The 23 staging rows come in five variants (the dialog's create and edit paths, with or without an owner on the units, and lot 62's own end-to-end key); tools/scripts/test/fixtures/allocation-keys-staging-shapes.mjs reproduces all 44 documents shape for shape with synthetic values.
Reported, never written: a key stamped _class that still has a trait, a key without _id, a value the log or the guard could not carry (undefined, an integer-like key, a $ or dotted key), a non-empty sub-tree (dropping it is a decision, not a migration), a unit object without a catalog id, a non-finite amount, a date that is not a day, and — defensive — a conversion whose DTO would differ.
The staging dry-run (2026-10-08, read-only)
== allocation keys — adb-utilities.allocationkeys
documents: 44 JVM-shaped (never touched): 21 legacy: 23
legacy traits: catalog-object: 23, number-amount: 23, string-date: 23, nested-shares: 23, display-fields: 23
to rewrite: 23
reported (left as is): 0
left alone in all: 21
gaps.md § AK-1008 counted 21 legacy rows earlier the same day; two more (6ac7582b939ad742f0d76a44, 6ac75848939ad742f0d76a49) were written before lot 62 was deployed. Re-run the dry-run right before --apply: it is the count that matters.
Prerequisites
- Lot 62 is deployed on the target (
services/utilitieswrites the entity). Otherwise new legacy rows keep appearing; a later re-run picks them up. - A dry-run of the target cluster has been read.
- A mongodump of
adb-utilities.allocationkeys, taken right before--apply. pnpm installat the repository root (the script needs themongodbdriver of@adb/scripts).- The connection string in
MONGO_URI, exported in a shell that is not logged (--urirefuses credentials).
Steps — staging
export MONGO_URI='mongodb+srv://…@adb-stage.cm0co.mongodb.net/'
node tools/scripts/migrate-allocation-key-shape.mjs | tee stage-dry-run.txt # read the header: the cluster, non-production
node tools/scripts/migrate-allocation-key-shape.mjs --verbose --limit 1 # one key before / after
mongodump --uri "$MONGO_URI" --db adb-utilities --collection allocationkeys --out "backup-$(date +%Y%m%d)"
node tools/scripts/migrate-allocation-key-shape.mjs --apply --limit 1 --log stage-allocation-keys.jsonl
node tools/scripts/migrate-allocation-key-shape.mjs --apply --log stage-allocation-keys.jsonl
node tools/scripts/migrate-allocation-key-shape.mjs # verify: legacy 0, to rewrite 0
Per key: PLAN (or REPORT with its category and reason) names the key, its building and its state. Under --apply the intent (before + after) is appended to the log, then the guarded replaceOne, then its result; a key changed since it was read is SKIPPED and logged as such. Re-run the same command after an interruption: done keys are JVM-shaped and no longer selected. Then open the « Clés de répartition » of building 64c11316be873b04f9adf176 in Angular (stage): the list answers instead of 400.
Steps — production
The Worker has never written production's allocationkeys, so the dry-run there is the whole job: it must show legacy: 0.
node tools/scripts/migrate-allocation-key-shape.mjs --i-know-this-is-prod | tee prod-dry-run.txt
If it does not, stop and escalate before applying.
Rollback
node tools/scripts/migrate-allocation-key-shape.mjs --rollback stage-allocation-keys.jsonl # what it would restore
node tools/scripts/migrate-allocation-key-shape.mjs --rollback stage-allocation-keys.jsonl --apply
Newest entry first, each restore is a replaceOne of the logged document guarded by the migrated one as a whole: a key changed since the migration is left alone (not-in-migrated-state), an entry whose own write did not land is skipped, an entry without a result line is attempted and counted uncertain. A second rollback restores nothing. Every log entry is checked before anything is restored — legacy before, JVM entity after, the same _id, the same API DTO — so a hand-edited log cannot restore another key's content; the check is by meaning, not by replaying the mapper, so a later change of lot 62's mapper does not strand a rollback. The rollback refuses a log recorded against another cluster, and the same production guard applies.
Open questions
- Non-empty sub-trees. None exists on staging. If one appears elsewhere it is reported: the JVM keeps one rolled-up share per target, and whether the parent share's amount equals its children's sum must be looked at before the sub-tree is dropped.
AllocationObjectEntity.sharesexists in the JVM entity but no mapper ever fills it; lot 62 and this migration follow the mapper, not the entity class.