Skip to content

Failed to update a unique relation at "relation name": the foreign record is already associated with another owner #353

@danielb7390

Description

@danielb7390

Hi,
Im trying to do a simple update of a Collection that has a one relation to other collection and it's failing with Failed to update a unique relation at "language": the foreign record is already associated with another owner.
Not sure if im doing something wrong, i even added the unique: false to ensure it wasn't that.
Copilot seems to belive theres a bug but i'm not 100% sure thats the fix, or even if theres something to fix in the first place (will leave it at the end of the issue in case its helpful, it makes the issue go away thats for sure)

Below a simplified example code and the output.

import { Collection } from '@msw/data';
import { z } from 'zod';

const languageSchema = z.object({
  iso3166: z.string(),
});

const languages = new Collection({ schema: languageSchema });

const userSchema = z.object({
  name: z.string(),
  // This is optional because of issue #348
  get language() { return languageSchema.optional(); },
});

const users = new Collection({ schema: userSchema });

users.defineRelations(({ one }) => ({
  language: one(languages, { unique: false }),
}));

void (async () => {
  const langPT = await languages.create({
    iso3166: 'pt',
  });
  console.log('🚀 ~ langPT:', langPT)

  const langEN = await languages.create({
    iso3166: 'en',
  });

  console.log('🚀 ~ langEN:', langEN)

  const user1 = await users.create({
    name: 'User 1',
    language: langPT,
  });
  console.log('🚀 ~ user1:', user1)

  const user2 = await users.create({
    name: 'User 2',
    language: langEN,
  });
  console.log('🚀 ~ user2:', user2)

  const all1 = users.findMany()
  console.log('🚀 ~ all1:', JSON.stringify(all1, null, 2))

  // Update the user1 to have language EN instead of PT
  // This fails!
  await users.update(
    user1,
    {
      data: (x) => {
        x.language = langEN;
      }
    }
  );

  const all2 = users.findMany()
  console.log('🚀 ~ all2:', JSON.stringify(all2, null, 2))

  // Update the user1 to have language PT instead of EN
  await users.update(
    user1,
    {
      data: (x) => {
        x.language = langPT;
      }
    }
  );

  const all3 = users.findMany()
  console.log('🚀 ~ all3:', JSON.stringify(all3, null, 2))
})();

The output is:

🚀 ~ langPT: { iso3166: 'pt' }
🚀 ~ langEN: { iso3166: 'en' }
🚀 ~ user1: { name: 'Portugal', language: [Getter/Setter] }
🚀 ~ user2: { name: 'United Kingdom', language: [Getter/Setter] }
/home/redacted/node_modules/@msw/data/build/errors-CVsx5ebH.js:31
                        return new RelationError(message, code, details);
                               ^

RelationError: Failed to update a unique relation at "language": the foreign record is already associated with another owner
    at <anonymous> (/home/redacted/node_modules/@msw/data/build/errors-CVsx5ebH.js:31:11)
    at invariant.invariant.as (/home/redacted/node_modules/outvariant/src/invariant.ts:76:16)
    at Emitter.<anonymous> (/home/redacted/node_modules/@msw/data/build/collection-BJduKbwC.js:226:16)
    at Emitter.#callListener (/home/redacted/node_modules/rettime/build/index.js:225:34)
    at Emitter.emit (/home/redacted/node_modules/rettime/build/index.js:83:12)
    at Collection.#produceRecord (/home/redacted/node_modules/@msw/data/build/collection-BJduKbwC.js:772:15)
    at async Collection.update (/home/redacted/node_modules/@msw/data/build/collection-BJduKbwC.js:498:22)
    at async <anonymous> (/home/redacted/test.ts:47:3) {
  code: 'FORBIDDEN_UNIQUE_UPDATE',
  details: {
    path: [ 'language' ],
    ownerCollection: Collection {
      options: {
        schema: ZodObject {
          (................)
        }
      },
      hooks: Emitter {},
      Symbol(kCollectionId): 3497938686528596
    },
    foreignCollections: [
      Collection {
        options: {
          schema: ZodObject {
                      (................)
          }
        },
        hooks: Emitter {},
        Symbol(kCollectionId): 1485272252048968
      }
    ],
    options: { unique: false }
  }
}

Node.js v24.13.0

Copilot suggestion:

This is a bug in @msw/data. Line 226 in the library runs an "already associated with another owner" check unconditionally, ignoring your unique: false option. The guard at line 223 only protects the first check:

// Line 223 – correctly guarded:
if (this.options.unique) invariant.as(..., foreignRelationsToDisassociate.length === 0, ...);

// Line 226 – BUG: always runs, ignores unique: false
const otherOwnersAssociatedWithForeignRecord = this.#getOtherOwnerForRecords([update.nextValue]);
invariant.as(..., otherOwnersAssociatedWithForeignRecord == null, ...);  // always throws!

This does fix the issue but then reveals another one, where both user1 and user2 end up with the same language in the last update. Copilot also "fixed" that, again im very unsure this is the correct way or not, i leave the diff here in case its helpful:

diff --git a/node_modules/@msw/data/build/collection-BJduKbwC.js b/node_modules/@msw/data/build/collection-BJduKbwC.js
index ba7e339..7d7e661 100644
--- a/node_modules/@msw/data/build/collection-BJduKbwC.js
+++ b/node_modules/@msw/data/build/collection-BJduKbwC.js
@@ -211,6 +211,7 @@ var Relation = class {
 		this.ownerCollection.hooks.on("update", (event) => {
 			const update = event.data;
 			if (isEqual(update.path, path) && isRecord(update.nextValue)) {
+				if (update.prevRecord[kRelationMap].get(serializedPath) !== this) return;
 				event.preventDefault();
 				if (this instanceof One) {
 					const foreignRelationsToDisassociate = this.foreignCollections.flatMap((foreignCollection) => {
@@ -223,7 +224,7 @@ var Relation = class {
 					if (this.options.unique) invariant.as(RelationError.for(RelationErrorCodes.FORBIDDEN_UNIQUE_UPDATE, this.#createErrorDetails()), foreignRelationsToDisassociate.length === 0, "Failed to update a unique relation at \"%s\": the foreign record is already associated with another owner", update.path.join("."));
 					for (const foreignRelation of foreignRelationsToDisassociate) foreignRelation.foreignKeys.delete(update.prevRecord[kPrimaryKey]);
 					const otherOwnersAssociatedWithForeignRecord = this.#getOtherOwnerForRecords([update.nextValue]);
-					invariant.as(RelationError.for(RelationErrorCodes.FORBIDDEN_UNIQUE_UPDATE, this.#createErrorDetails()), otherOwnersAssociatedWithForeignRecord == null, "Failed to update a unique relation at \"%s\": the foreign record is already associated with another owner", update.path.join("."));
+					if (this.options.unique) invariant.as(RelationError.for(RelationErrorCodes.FORBIDDEN_UNIQUE_UPDATE, this.#createErrorDetails()), otherOwnersAssociatedWithForeignRecord == null, "Failed to update a unique relation at \"%s\": the foreign record is already associated with another owner", update.path.join("."));
 					this.foreignKeys.clear();
 				}
 				const foreignRecord = update.nextValue;

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions