Skip to content
Astera Technology

Why did a retained relative attachment link point to the wrong file after migration?

Resolve against the old base, map the actual object, then create the reference for its new location.

Download complete case notes and answer keys ↓

Read the source and its declared rule

The original pack’s docs/record refers to ../files/a. Its mapped destination is target/assets/a.bin, while the new record sits under target/pages. Reusing the same relative spelling incorrectly reaches target/files/a. Internal references require a supplied object map; external origins remain external under this fixture rule.

Filled synthetic input
RecordInput JSON
Base input{"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin"}]}
Download editable CSV ↓

Compare the actual paths

Correct new reference ../assets/a.bin resolves to the mapped file. Retain the mapped destination’s query and fragment when constructing a relative reference. All example.invalid addresses are labels for local URL resolution and are never requested.

Misleading and corrected outputs
PathOutput JSON
Misleading path{"accepted":true,"reference":"../files/a","resolved":"https://files.example.invalid/target/files/a"}
Corrected path{"accepted":true,"reference":"../assets/a.bin","resolved":"https://files.example.invalid/target/assets/a.bin"}
Download editable CSV ↓
Local Node.js program: 22.23.3 or 24.14.1
import {DatabaseSync} from 'node:sqlite';
import {TextDecoder} from 'node:util';
import {inflateSync} from 'node:zlib';
import {posix} from 'node:path';
import {createHash} from 'node:crypto';
const rows=(db,sql)=>db.prepare(sql).all().map(r=>({...r}));
const failed=(action)=>{try{action();return null;}catch(e){return {code:e.errcode??e.code??e.name};}};

import assert from 'node:assert/strict';
const run=function run(i,corrected=true){
    if(!corrected)return {accepted:true,reference:i.reference,resolved:new URL(i.reference,i.newBase).href};
    const old=new URL(i.reference,i.oldBase);const oldBase=new URL(i.oldBase);
    if(old.origin!==oldBase.origin)return {accepted:true,reference:i.reference,resolved:old.href};
    const target=i.mapping.find(m=>m.source===old.href);
    if(!target)return {accepted:false,error:'unmapped-internal-reference',original:i.reference,resolvedSource:old.href};
    const dest=new URL(target.destination);const newBase=new URL(i.newBase);
    const reference=dest.origin===newBase.origin?posix.relative(posix.dirname(newBase.pathname),dest.pathname)+dest.search+dest.hash:dest.href;
    return {accepted:true,reference,resolved:new URL(reference,i.newBase).href};
  };
const cases=[{"label":"misleading base","input":{"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin"}]},"expected":{"accepted":true,"reference":"../files/a","resolved":"https://files.example.invalid/target/files/a"},"corrected":false},{"label":"corrected base","input":{"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin"}]},"expected":{"accepted":true,"reference":"../assets/a.bin","resolved":"https://files.example.invalid/target/assets/a.bin"},"corrected":true},{"label":"external absolute reference retained","input":{"reference":"https://outside.example.invalid/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[]},"expected":{"accepted":true,"reference":"https://outside.example.invalid/a","resolved":"https://outside.example.invalid/a"},"corrected":true},{"label":"unmapped internal object held","input":{"reference":"../files/missing","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[]},"expected":{"accepted":false,"error":"unmapped-internal-reference","original":"../files/missing","resolvedSource":"https://files.example.invalid/pack/files/missing"},"corrected":true},{"label":"mapped query and fragment retained","input":{"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin?revision=2#part"}]},"expected":{"accepted":true,"reference":"../assets/a.bin?revision=2#part","resolved":"https://files.example.invalid/target/assets/a.bin?revision=2#part"},"corrected":true}];
for(const test of cases){
  const actual=run(test.input,test.corrected);
  assert.deepEqual(actual,test.expected,test.label);
  console.log(test.label,actual);
}
Download runnable example ↓

Check boundaries before the next load

An external absolute reference stays unchanged. An unmapped internal reference is held with its resolved old target. A mapped ?revision=2#part stays in both the new reference and resolved URL.

Editable boundary records and expected outputs
CaseInput JSONExpected output JSON
External absolute reference{"reference":"https://outside.example.invalid/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[]}{"accepted":true,"reference":"https://outside.example.invalid/a","resolved":"https://outside.example.invalid/a"}
Internal object lacks a map{"reference":"../files/missing","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[]}{"accepted":false,"error":"unmapped-internal-reference","original":"../files/missing","resolvedSource":"https://files.example.invalid/pack/files/missing"}
Mapped query and fragment{"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin?revision=2#part"}]}{"accepted":true,"reference":"../assets/a.bin?revision=2#part","resolved":"https://files.example.invalid/target/assets/a.bin?revision=2#part"}
Download editable CSV ↓

Should the new record simply keep ../files/a?

Show the worked answer

No. Its new base changes that destination. Use the mapped ../assets/a.bin reference instead.

Related resources