Coding challenge - Part Position answered by Theofanis Vardatsikos (vardtheo@gmail.com).
What potential actions do you identify as requiring a recalculation of positions in the Parts linked to the Episode?
Actions create
and update
require recalculation of positions. For update
only when the position
is changed.
Position would not need to be sequential in order for indexing to work e.g., 1,4,5,7 would be acceptable, then delete
does not need recalculation.
However, if position is required to be sequential then recalculation is needed also in delete
.
I assume another endpoint exists solely for sorting all the parts, in UI terms it could be a list with draggable records, then recalculation is needed at the endpoint.
What API Request payload structure would you use to reliable place the Parts in the position they should end up on? Consider separate endpoints to add, delete and sort Parts.
For adding Parts
- I would suggest to simply add the
position
property in the request sent.- This might produce false result when two users send the request simultaneously with the same position. In this occasion one of them would be moved one position after the other. Finally, we would have to recalculate the position for the ones after it.
- Another method would be to send
... "position_after": 1 ...
.position_after
is the id of the previous Part, so this new Part must be added after that existing part. It is like using the existing Part as pointer like a node in a linked list. A similar approach could be used forposition_before
.- However, if the existing part is deleted by the time the user submits the request then this could result in error.
- Also, if the existing part has been moved to another position until the user submits the request, then the new one will be implicitly moved. However, this might be the correct result if the two Parts have some logic correlation between each other.
- Third option would be to send an array of the ids of all the Parts. The sequence of each id would imply the position of the equivalent Part. One of these items would be a placeholder for the new item. This would result in setting up the absolute correct list, but possibly overwriting existing changes that were made until the form was submitted.
If I could have more information I could select the most appropriate option or even provide an alternative one better suited for your requirements.
For deletion simply delete the particular Part. If sequential positions are required then recalculate the position of all the Parts after it.
For sorting send an array of ids of all the Parts (the same as the third approach from addition). The sequence of each id in the array would imply the position of each Part. This would result in setting up the absolute correct list, but possibly overwriting existing changes that were made since the user loaded that page and until the form was submitted.
Implement the calculation of the new positions for Parts when changes are performed that affect them.
Ideally, the best approach would be to make all the necessary changes withing a single query. In this way we eliminate errors by minimising back-forth between PHP and SQL otherwise we would need to wrap those PHP statements in a SQL transaction. Additionally, limiting back-forth between PHP and SQL and not using transaction would result in significantly less execution time. Which in each turn would produce less delay for the consequent requests as it would lock the SQL tables for significantly less time, and that is an imperative requirement for repeating changes in a large dataset.
Having said all that, the following Laravel code updates the position of all Parts from an array of IDs like in sorting in the previous question in a single upsert statement.
$ids = [10, 13, 7, 18, 11]; // IDs of Parts in the same sequence as they should be positioned.
$upsert = array_map(fn(int $id, int $index) => ['id' => $id, 'position' => $index], $ids);
Part::upsert($upsert, ['id'], ['position']);
An alternative implementation (maybe not so performant) with the same result would be.
$ids = [8, 1, 5, 2, 3, 7]; // Sanitize contents for sql-injection
Part::whereIn('id', $ids)->update(['position' => DB::raw("FIND_IN_SET(id, '" . implode(',', $ids) . "')")]);
// UPDATE parts SET position = FIND_IN_SET(id, '8,1,5,2,3,7') WHERE id IN (8,1,5,2,3,7);
In the following code we change the position of one Part and recalculate the position for every Part in the range between the old and new position. The rest of the Parts [below min(new,old), above max(new,old)] remain unchanged since they are not affected. In a single query we change the position for the designated Part and increase/decrease all the other Parts by one according to whether the designated Part moved down/up respectively.
$part = Part::findOrFail($some_id);
$old_position = $part->position;
$new_position = 3; // Recalculate all Parts between old and new position.
$sign = $old_position < $new_position ? -1 : 1; // Whether the rest of the Parts need to be increased/decreased.
Part::where('episode_id', $part->episode_id)
->whereBetween('position', [min($new_position, $old_position), max($new_position, $old_position)])
->update(['position' => DB::raw("IF(id = {$part->id}, $new_position, position + $sign)")]);
Another solution would be to create endpoints for increasing/decreasing position by one, or exchanging the position between two specific Parts. These solutions have straight-forward implementation with limited room for error. However, I am skeptical about these solutions, because I do not believe they would be particularly useful for the users.
I do not have enough information about the infrastructure, the needs of the users, or how they use the platform. I could definitely provide more refined solutions had been given more details about the issue. Nevertheless, the previous solutions should be adequate as a starting point.
Thank you for your consideration, looking forward to your assessment❗️