@@ -245,27 +245,42 @@ export abstract class Relation {
245245 } ,
246246 )
247247
248+ const foreignRelationsToDisassociate = oldForeignRecords . flatMap (
249+ ( record ) => this . getRelationsToOwner ( record ) ,
250+ )
251+
248252 // Throw if attempting to disassociate unique relations.
249253 if ( this . options . unique ) {
250254 invariant . as (
251255 RelationError . for (
252256 RelationErrorCodes . FORBIDDEN_UNIQUE_UPDATE ,
253257 this . #createErrorDetails( ) ,
254258 ) ,
255- oldForeignRecords . length === 0 ,
259+ foreignRelationsToDisassociate . length === 0 ,
256260 'Failed to update a unique relation at "%s": the foreign record is already associated with another owner' ,
257261 update . path . join ( '.' ) ,
258262 )
259263 }
260264
261- const foreignRelationsToDisassociate = oldForeignRecords . flatMap (
262- ( record ) => this . getRelationsToOwner ( record ) ,
263- )
264-
265265 for ( const foreignRelation of foreignRelationsToDisassociate ) {
266266 foreignRelation . foreignKeys . delete ( update . prevRecord [ kPrimaryKey ] )
267267 }
268268
269+ // Check any other owners associated with the same foreign record.
270+ // This is important since unique relations are not always two-way.
271+ const otherOwnersAssociatedWithForeignRecord =
272+ this . #getOtherOwnerForRecords( [ update . nextValue ] )
273+
274+ invariant . as (
275+ RelationError . for (
276+ RelationErrorCodes . FORBIDDEN_UNIQUE_UPDATE ,
277+ this . #createErrorDetails( ) ,
278+ ) ,
279+ otherOwnersAssociatedWithForeignRecord == null ,
280+ 'Failed to update a unique relation at "%s": the foreign record is already associated with another owner' ,
281+ update . path . join ( '.' ) ,
282+ )
283+
269284 this . foreignKeys . clear ( )
270285 }
271286
@@ -316,24 +331,19 @@ export abstract class Relation {
316331 initialValue ,
317332 )
318333
319- const initialForeignEntries : Array < RecordType > = Array . prototype
334+ const initialForeignRecords : Array < RecordType > = Array . prototype
320335 . concat ( [ ] , get ( initialValues , path ) )
321336 /**
322337 * @note If the initial value as an empty array, concatenating it above
323338 * results in [undefined]. Filter out undefined values.
324339 */
325340 . filter ( Boolean )
326341
327- logger . log ( 'all foreign entries:' , initialForeignEntries )
342+ logger . log ( 'all foreign entries:' , initialForeignRecords )
328343
329- /**
330- * @todo Check if:
331- * 1. Relation is unique;
332- * 2. Foreign entries already reference something!
333- * Then, throw.
334- */
335344 if ( this . options . unique ) {
336- const foreignRelations = initialForeignEntries . flatMap (
345+ // Check if the foreign record isn't associated with another owner.
346+ const foreignRelations = initialForeignRecords . flatMap (
337347 ( foreignRecord ) => {
338348 return this . getRelationsToOwner ( foreignRecord )
339349 } ,
@@ -343,20 +353,32 @@ export abstract class Relation {
343353 ( relation ) => relation . foreignKeys . size === 0 ,
344354 )
345355
346- const recordLabel = this instanceof Many ? 'records' : 'record'
347-
348356 invariant . as (
349357 RelationError . for (
350358 RelationErrorCodes . FORBIDDEN_UNIQUE_CREATE ,
351359 this . #createErrorDetails( ) ,
352360 ) ,
353361 isUnique ,
354- `Failed to create a unique relation at "%s": foreign ${ recordLabel } already associated with another owner` ,
355- this . path . join ( '.' ) ,
362+ `Failed to create a unique relation at "%s": foreign ${ this instanceof Many ? 'records' : 'record' } already associated with another owner` ,
363+ serializedPath ,
364+ )
365+
366+ // Check if another owner isn't associated with the foreign record.
367+ const otherOwnersAssociatedWithForeignRecord =
368+ this . #getOtherOwnerForRecords( initialForeignRecords )
369+
370+ invariant . as (
371+ RelationError . for (
372+ RelationErrorCodes . FORBIDDEN_UNIQUE_CREATE ,
373+ this . #createErrorDetails( ) ,
374+ ) ,
375+ otherOwnersAssociatedWithForeignRecord == null ,
376+ 'Failed to create a unique relation at "%s": the foreign record is already associated with another owner' ,
377+ serializedPath ,
356378 )
357379 }
358380
359- for ( const foreignRecord of initialForeignEntries ) {
381+ for ( const foreignRecord of initialForeignRecords ) {
360382 const foreignKey = foreignRecord [ kPrimaryKey ]
361383
362384 invariant . as (
@@ -448,6 +470,25 @@ export abstract class Relation {
448470 return result
449471 }
450472
473+ #getOtherOwnerForRecords(
474+ foreignRecords : Array < RecordType > ,
475+ ) : RecordType | undefined {
476+ const serializedPath = this . path . join ( '.' )
477+
478+ return this . ownerCollection . findFirst ( ( q ) => {
479+ return q . where ( ( otherOwner ) => {
480+ const otherOwnerRelations = otherOwner [ kRelationMap ]
481+ const otherOwnerRelation = otherOwnerRelations . get ( serializedPath )
482+
483+ // Forego any other relation comparisons since the same collection
484+ // shares the relation definition at the same property path.
485+ return foreignRecords . some ( ( foreignRecord ) => {
486+ return otherOwnerRelation . foreignKeys . has ( foreignRecord [ kPrimaryKey ] )
487+ } )
488+ } )
489+ } )
490+ }
491+
451492 #createErrorDetails( ) : RelationErrorDetails {
452493 return {
453494 path : this . path ,
0 commit comments