From b9ab35a05bae1b92c46e04d4a58e4553fbaca9ea Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 11 May 2026 16:57:53 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20handle=20idempotency=20when=20ad?= =?UTF-8?q?ding/removing=20labels=20from=20sites/resources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 60 +++++++++++-------- .../routers/labels/attachLabelToItem.ts | 15 +++-- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 0a4066db3..a797e3ddc 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -173,33 +173,41 @@ export const labels = pgTable("labels", { .notNull() }); -export const siteLabels = pgTable("siteLabels", { - siteLabelId: serial("siteLabelId").primaryKey(), - siteId: integer("siteId") - .references(() => sites.siteId, { - onDelete: "cascade" - }) - .notNull(), - labelId: integer("labelId") - .references(() => labels.labelId, { - onDelete: "cascade" - }) - .notNull() -}); +export const siteLabels = pgTable( + "siteLabels", + { + siteLabelId: serial("siteLabelId").primaryKey(), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), + labelId: integer("labelId") + .references(() => labels.labelId, { + onDelete: "cascade" + }) + .notNull() + }, + (t) => [unique("site_label_uniq").on(t.siteId, t.labelId)] +); -export const resourceLabels = pgTable("resourceLabels", { - resourceLabelId: serial("resourceLabelId").primaryKey(), - resourceId: integer("resourceId") - .references(() => resources.resourceId, { - onDelete: "cascade" - }) - .notNull(), - labelId: integer("labelId") - .references(() => labels.labelId, { - onDelete: "cascade" - }) - .notNull() -}); +export const resourceLabels = pgTable( + "resourceLabels", + { + resourceLabelId: serial("resourceLabelId").primaryKey(), + resourceId: integer("resourceId") + .references(() => resources.resourceId, { + onDelete: "cascade" + }) + .notNull(), + labelId: integer("labelId") + .references(() => labels.labelId, { + onDelete: "cascade" + }) + .notNull() + }, + (t) => [unique("resource_label_uniq").on(t.resourceId, t.labelId)] +); export const targets = pgTable("targets", { targetId: serial("targetId").primaryKey(), diff --git a/server/private/routers/labels/attachLabelToItem.ts b/server/private/routers/labels/attachLabelToItem.ts index 392332776..79ea360de 100644 --- a/server/private/routers/labels/attachLabelToItem.ts +++ b/server/private/routers/labels/attachLabelToItem.ts @@ -106,13 +106,14 @@ export async function attachLabelToItem( ); } + // idempotent, calling this endpoint multiple times should attach the label only once await db .insert(siteLabels) .values({ labelId, siteId }) - .returning(); + .onConflictDoNothing(); } if (resourceId) { @@ -133,10 +134,14 @@ export async function attachLabelToItem( ); } - await db.insert(resourceLabels).values({ - labelId, - resourceId - }); + // idempotent, calling this endpoint multiple times should attach the label only once + await db + .insert(resourceLabels) + .values({ + labelId, + resourceId + }) + .onConflictDoNothing(); } return response(res, {