mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 22:22:24 -05:00
fix: guard tag rewrites against concurrent edits, pluralise merge count, fix lt wording
This commit is contained in:
1 parent
a0dbba8058
commit
ef14f54221
7 files changed
+55
-19
No files matched your search
@@ -26,13 +26,32 @@ function rewrite(tags: string[], from: Set<string>, to: string | null): string[]
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Swaps `from` for `to` (or removes them) on every model and collection of the user's, in one transaction so two
|
||||
* rewrites can't each save a model's tags over the other's. Answers how many models had each of `from`, the models
|
||||
* changed, and how many of those hadn't had `to`. */
|
||||
/** Thrown inside a rewrite when a row's tags changed after they were read, to roll the rewrite back and retry it. */
|
||||
class TagsChanged extends Error {}
|
||||
|
||||
/** Swaps `from` for `to` (or removes them) on every model and collection of the user's, in one transaction. Each
|
||||
* write only lands if the row still has the tags that were read, so a concurrent edit is never saved over: the
|
||||
* rewrite is rolled back and retried instead. Answers how many models had each of `from`, the models changed, and
|
||||
* how many of those hadn't had `to`. */
|
||||
async function rewriteTags(
|
||||
userId: string,
|
||||
from: Set<string>,
|
||||
to: string | null,
|
||||
): Promise<{ counts: Map<string, number>; printIds: string[]; gained: number }> {
|
||||
for (let attempt = 1; ; attempt++) {
|
||||
try {
|
||||
return await rewriteTagsOnce(userId, from, to);
|
||||
} catch (err) {
|
||||
if (!(err instanceof TagsChanged)) throw err;
|
||||
if (attempt >= 3) throw new HttpError(409, "Tags changed while saving, try again");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function rewriteTagsOnce(
|
||||
userId: string,
|
||||
from: Set<string>,
|
||||
to: string | null,
|
||||
): Promise<{ counts: Map<string, number>; printIds: string[]; gained: number }> {
|
||||
const [prints, collections] = await Promise.all([
|
||||
prisma.print.findMany({ where: { userId }, select: { id: true, tags: true } }),
|
||||
@@ -48,14 +67,25 @@ async function rewriteTags(
|
||||
return hit;
|
||||
});
|
||||
const taggedCollections = collections.filter((c) => c.tags.some((tag) => from.has(normalizeTag(tag))));
|
||||
await prisma.$transaction([
|
||||
...tagged.map((print) =>
|
||||
prisma.print.update({ where: { id: print.id }, data: { tags: rewrite(print.tags, from, to) } }),
|
||||
),
|
||||
...taggedCollections.map((c) =>
|
||||
prisma.collection.update({ where: { id: c.id }, data: { tags: rewrite(c.tags, from, to) } }),
|
||||
),
|
||||
]);
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
for (const print of tagged) {
|
||||
const { count } = await tx.print.updateMany({
|
||||
where: { id: print.id, tags: { equals: print.tags } },
|
||||
data: { tags: rewrite(print.tags, from, to) },
|
||||
});
|
||||
if (!count) throw new TagsChanged();
|
||||
}
|
||||
for (const c of taggedCollections) {
|
||||
const { count } = await tx.collection.updateMany({
|
||||
where: { id: c.id, tags: { equals: c.tags } },
|
||||
data: { tags: rewrite(c.tags, from, to) },
|
||||
});
|
||||
if (!count) throw new TagsChanged();
|
||||
}
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
return { counts, printIds: tagged.map((print) => print.id), gained };
|
||||
}
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ export default function MergeDialog({ open, onClose, kind, items, loadPreview, o
|
||||
{added !== null && added > 0 && (
|
||||
<Box component="span" sx={{ color: "success.main", fontWeight: 600 }}>
|
||||
{" · +"}
|
||||
<RollingNumber value={added} /> {t("models:merge.new")}
|
||||
<RollingNumber value={added} /> {t("models:merge.new", { count: added })}
|
||||
</Box>
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
@@ -57,9 +57,9 @@ export function useSelection<T>(
|
||||
const from = anchor ? pickable.findIndex((item) => keyOf(item) === anchor) : -1;
|
||||
const to = pickable.findIndex((item) => keyOf(item) === key);
|
||||
if (to < 0) return prev;
|
||||
if (range && from >= 0) {
|
||||
if (range && anchor !== null && from >= 0) {
|
||||
// The range takes the state the anchor was left in, as a file manager does.
|
||||
const select = prev.has(anchor!);
|
||||
const select = prev.has(anchor);
|
||||
for (const item of pickable.slice(Math.min(from, to), Math.max(from, to) + 1)) {
|
||||
if (select) next.add(keyOf(item));
|
||||
else next.delete(keyOf(item));
|
||||
|
||||
@@ -3106,7 +3106,8 @@
|
||||
"after": "After the merge",
|
||||
"modelUnit_one": "model",
|
||||
"modelUnit_other": "models",
|
||||
"new": "new",
|
||||
"new_one": "new",
|
||||
"new_other": "new",
|
||||
"quoted": "“{{name}}”",
|
||||
"andMore_one": "{{count}} more",
|
||||
"andMore_other": "{{count}} more",
|
||||
|
||||
@@ -3107,7 +3107,8 @@
|
||||
"after": "Tras la fusión",
|
||||
"modelUnit_one": "modelo",
|
||||
"modelUnit_other": "modelos",
|
||||
"new": "nuevos",
|
||||
"new_one": "nuevo",
|
||||
"new_other": "nuevos",
|
||||
"quoted": "«{{name}}»",
|
||||
"andMore_one": "{{count}} más",
|
||||
"andMore_other": "{{count}} más",
|
||||
|
||||
@@ -3120,7 +3120,8 @@
|
||||
"after": "Après la fusion",
|
||||
"modelUnit_one": "modèle",
|
||||
"modelUnit_other": "modèles",
|
||||
"new": "nouveaux",
|
||||
"new_one": "nouveau",
|
||||
"new_other": "nouveaux",
|
||||
"quoted": "« {{name}} »",
|
||||
"andMore_one": "{{count}} autre",
|
||||
"andMore_other": "{{count}} autres",
|
||||
|
||||
@@ -3234,14 +3234,17 @@
|
||||
"modelUnit_few": "modeliai",
|
||||
"modelUnit_many": "modelio",
|
||||
"modelUnit_other": "modelių",
|
||||
"new": "naujų",
|
||||
"new_one": "naujas",
|
||||
"new_few": "nauji",
|
||||
"new_many": "naujo",
|
||||
"new_other": "naujų",
|
||||
"quoted": "„{{name}}“",
|
||||
"andMore_one": "dar {{count}}",
|
||||
"andMore_few": "dar {{count}}",
|
||||
"andMore_many": "dar {{count}}",
|
||||
"andMore_other": "dar {{count}}",
|
||||
"syncsWith": "sinchronizuojama su {{provider}}",
|
||||
"bookmarkMoves": "Įsiminimas pereina „{{name}}“.",
|
||||
"bookmarkMoves": "Įsiminimas pereina į „{{name}}“.",
|
||||
"cantUndo": "Sujungimo atšaukti negalima.",
|
||||
"mergeInto": "Sujungti į „{{name}}“",
|
||||
"collections": {
|
||||
|
||||
Reference in new issue
Block a user