Using References Without Bugs. An Example of Id Transformation.
Alexandre Fauquette3 min read
As a coder, you probably use a lot of ids. Creating them is so easy. But when we have to modify one, keeping your data consistent can become a nightmare. This article presents our 10 lines solution.
Why modifying ids?
I am working on a project named Splane which helps to improve lead time by visualizing potentials problems. The main part is a board containing epics that are dev’ objectives and dependencies. When two objects are linked, the link is stored by putting the dependency id in a list of dependencies. Bellow, a toy example and its associated .json file.
Two epics linked to dependencies. Their graphical representation and the data structure.
"board": {
"id": "myBoardId",
"name": "myBoard",
"epics": [
{"id": 53, "dependencies": [134]},
{"id": 42, "dependencies": [254]}
],
"dependencies": [
{"id": 134},
{"id": 254}
],
}
Imagine you want to duplicate your board. We want to copy the structure and the content. But ids should be unique, so we want to generate new ids. It’s quite simple:
import * as cuid from 'cuid';//id generator
//in Board object
duplicate() {
return new Board({
id: cuid(),
name: `${this.name} - Copy`,
epics: this.epics.map(epic=>epic.duplicate()),
dependencies: this.dependencies.map(dep=>dep.duplicate())
});
}
//in Epic object
duplicate() {
return new Epic({
id: cuid(),
name: this.name,
dependenciesId: [...this.dependenciesId] // Bad! dependencyId unchanged
});
}
//in Dependency object
duplicate() {
return new Dependency({
id: cuid(),
name: this.name,
});
}
Here the nightmare starts: Dependencies have new ids. But the list “dependenciesId” contained by Epic objects still contains old ids. By this operation, we broke all the links present in our data structure.
A basic solution
The main idea is to create a kind of big table mapping previous ids to new ones. However, it means:
- Implementing a tree traversal on our board object to find all ids.
- Creating the translation table
- Duplicating the board using this translation table.
It does not seem to be a graceful solution.
Fortunately, Javascript references exist. With them, we can create a function that fills the table and use it to translate ids. In short, it does all we want. Moreover, it contains less than ten lines.
//Our magic function
const createTransformId = (mapOldToNewIds) => (id) => {
if (mapOldToNewIds[id] === undefined) {//unknow id ?
mapOldToNewIds[id] = cuid();//no probleme, I generate a new one and save it
}
return mapOldToNewIds[id];
};
Notice that we created only one instance of the table. So we have only one source of truth. The logic of id creation and reuse is defined in our function. Now, we will use it to duplicate the board. Each time there is an id, we use the function to translate it.
//in Board object
duplicate() {
const mapOldToNewIds = {};
const transformId = createTransformId(mapOldToNewIds);
return new Board({
id: transformId(this.id),
name: `${this.name} - Copy`,
epics: this.epics.map(epic=>epic.duplicate(transformId)),//pass transformId function to child elements
dependencies: this.dependencies.map(dep=>dep.duplicate(transformId))
});
}
Board is the root object. So we initialized the mapOldToNewIds
in its duplicate
method. For others, we want to refer to this mapOldToNewIds
. To do that, duplicate
methods get the function’s reference transformId
as an argument.
//in Epic object
duplicate(transformId) {
return new Epic({
id: transformId(this.id),
name: this.name,
dependenciesId: this.dependenciesId.map(depId => transformId(depId)) //Good! ids will be consistents with dependencies id
});
}
//in Dependency onject
duplicate(transformId) {
return new Dependency({
id: transformId(this.id),
name: this.name,
});
}
Of course, the data structure of Splane boards is much more complex than what we presented. But this method scales very well because we can go as deep as we want in the tree structure. For that, we just have to pass the function reference element by element.