@@ -141,47 +141,75 @@ export abstract class Relation {
141141 // This way, each record has its own instance of a stateful relation.
142142 this . #initializeRelation( path , record , initialValues )
143143
144+ /**
145+ * @note Tear down all hook listeners registered below once the owner
146+ * record is deleted. Aborting the signal removes the listeners from
147+ * the underlying emitter — including the `delete` listener itself.
148+ */
149+ const abortController = new AbortController ( )
150+
151+ this . ownerCollection . hooks . on (
152+ 'delete' ,
153+ ( event ) => {
154+ if (
155+ event . data . deletedRecord [ kRelationMap ] . get ( serializedPath ) === this
156+ ) {
157+ abortController . abort ( )
158+ }
159+ } ,
160+ { signal : abortController . signal } ,
161+ )
162+
144163 for ( const foreignCollection of this . foreignCollections ) {
145164 // Update the owner relations when a foreign record is created
146165 // referencing the owner record.
147- foreignCollection . hooks . on ( 'create' , ( event ) => {
148- const { record : foreignRecord } = event . data
149- const foreignRelations = this . getRelationsToOwner ( foreignRecord )
150-
151- for ( const foreignRelation of foreignRelations ) {
152- const ownerRecords = this . ownerCollection . findMany ( ( q ) =>
153- q . where ( ( record ) => {
154- return foreignRelation . foreignKeys . has ( record [ kPrimaryKey ] )
155- } ) ,
156- )
166+ foreignCollection . hooks . on (
167+ 'create' ,
168+ ( event ) => {
169+ const { record : foreignRecord } = event . data
170+ const foreignRelations = this . getRelationsToOwner ( foreignRecord )
171+
172+ for ( const foreignRelation of foreignRelations ) {
173+ const ownerRecords = this . ownerCollection . findMany ( ( q ) =>
174+ q . where ( ( record ) => {
175+ return foreignRelation . foreignKeys . has ( record [ kPrimaryKey ] )
176+ } ) ,
177+ )
157178
158- for ( const ownerRecord of ownerRecords ) {
159- const ownerRelation = ownerRecord [ kRelationMap ] . get ( serializedPath )
160- ownerRelation . foreignKeys . add ( foreignRecord [ kPrimaryKey ] )
179+ for ( const ownerRecord of ownerRecords ) {
180+ const ownerRelation =
181+ ownerRecord [ kRelationMap ] . get ( serializedPath )
182+ ownerRelation . foreignKeys . add ( foreignRecord [ kPrimaryKey ] )
183+ }
161184 }
162- }
163- } )
185+ } ,
186+ { signal : abortController . signal } ,
187+ )
164188
165189 // Clear the references to deleted foreign records.
166- foreignCollection . hooks . on ( 'delete' , ( event ) => {
167- const { deletedRecord : deletedForeignRecord } = event . data
168- this . foreignKeys . delete ( deletedForeignRecord [ kPrimaryKey ] )
169-
170- // Delete all the owners referencing the deleted foreign record
171- // if the relation is set to cascade on delete.
172- if ( this . options . onDelete === 'cascade' ) {
173- const foreignRelations =
174- this . getRelationsToOwner ( deletedForeignRecord )
175-
176- this . ownerCollection . deleteMany ( ( q ) => {
177- return q . where ( ( record ) => {
178- return foreignRelations . some ( ( foreignRelation ) => {
179- return foreignRelation . foreignKeys . has ( record [ kPrimaryKey ] )
190+ foreignCollection . hooks . on (
191+ 'delete' ,
192+ ( event ) => {
193+ const { deletedRecord : deletedForeignRecord } = event . data
194+ this . foreignKeys . delete ( deletedForeignRecord [ kPrimaryKey ] )
195+
196+ // Delete all the owners referencing the deleted foreign record
197+ // if the relation is set to cascade on delete.
198+ if ( this . options . onDelete === 'cascade' ) {
199+ const foreignRelations =
200+ this . getRelationsToOwner ( deletedForeignRecord )
201+
202+ this . ownerCollection . deleteMany ( ( q ) => {
203+ return q . where ( ( record ) => {
204+ return foreignRelations . some ( ( foreignRelation ) => {
205+ return foreignRelation . foreignKeys . has ( record [ kPrimaryKey ] )
206+ } )
180207 } )
181208 } )
182- } )
183- }
184- } )
209+ }
210+ } ,
211+ { signal : abortController . signal } ,
212+ )
185213 }
186214
187215 /**
@@ -192,126 +220,137 @@ export abstract class Relation {
192220 * @example
193221 * await users.update(q, { data: { country: { code: 'uk' } } })
194222 */
195- this . ownerCollection . hooks . earlyOn ( 'update' , ( event ) => {
196- const update = event . data
197-
198- if (
199- path . every ( ( key , index ) => key === update . path [ index ] ) &&
200- ! isRecord ( update . nextValue )
201- ) {
202- /**
203- * @note Listeners are attached per-record but fire for every owner update.
204- * Skip events whose target record's relation isn't this instance.
205- */
206- if ( update . prevRecord [ kRelationMap ] . get ( serializedPath ) !== this ) {
207- return
208- }
223+ this . ownerCollection . hooks . earlyOn (
224+ 'update' ,
225+ ( event ) => {
226+ const update = event . data
227+
228+ if (
229+ path . every ( ( key , index ) => key === update . path [ index ] ) &&
230+ ! isRecord ( update . nextValue )
231+ ) {
232+ /**
233+ * @note Listeners are attached per-record but fire for every owner update.
234+ * Skip events whose target record's relation isn't this instance.
235+ */
236+ if ( update . prevRecord [ kRelationMap ] . get ( serializedPath ) !== this ) {
237+ return
238+ }
209239
210- event . preventDefault ( )
211- event . stopImmediatePropagation ( )
240+ event . preventDefault ( )
241+ event . stopImmediatePropagation ( )
212242
213- const foreignUpdatePath = update . path . slice ( path . length )
243+ const foreignUpdatePath = update . path . slice ( path . length )
214244
215- for ( const foreignCollection of this . foreignCollections ) {
216- foreignCollection . updateMany (
217- ( q ) => {
218- return q . where ( ( record ) => {
219- return this . foreignKeys . has ( record [ kPrimaryKey ] )
220- } )
221- } ,
222- {
223- data ( foreignRecord ) {
224- set ( foreignRecord , foreignUpdatePath , update . nextValue )
245+ for ( const foreignCollection of this . foreignCollections ) {
246+ foreignCollection . updateMany (
247+ ( q ) => {
248+ return q . where ( ( record ) => {
249+ return this . foreignKeys . has ( record [ kPrimaryKey ] )
250+ } )
225251 } ,
226- } ,
227- )
252+ {
253+ data ( foreignRecord ) {
254+ set ( foreignRecord , foreignUpdatePath , update . nextValue )
255+ } ,
256+ } ,
257+ )
258+ }
228259 }
229- }
230- } )
260+ } ,
261+ { signal : abortController . signal } ,
262+ )
231263
232264 /**
233265 * Handle owner updates where the relational property changes to another foreign record.
234266 *
235267 * @example
236268 * await users.update(q, { data: { country: await countries.create({}) } })
237269 */
238- this . ownerCollection . hooks . on ( 'update' , ( event ) => {
239- const update = event . data
240-
241- if ( isEqual ( update . path , path ) && isRecord ( update . nextValue ) ) {
242- /**
243- * @note Listeners are attached per-record but fire for every owner update.
244- * Skip events whose target record's relation isn't this instance.
245- */
246- if ( update . prevRecord [ kRelationMap ] . get ( serializedPath ) !== this ) {
247- return
248- }
249-
250- event . preventDefault ( )
251-
252- // If the owner relation is "one-of", multiple foreign records cannot own this record.
253- // Disassociate the old foreign records from pointing to the owner record.
254- if ( this instanceof One ) {
255- const oldForeignRecords = this . foreignCollections . flatMap < RecordType > (
256- ( foreignCollection ) => {
257- return foreignCollection . findMany ( ( q ) => {
258- return q . where ( ( record ) => {
259- return this . foreignKeys . has ( record [ kPrimaryKey ] )
260- } )
261- } )
262- } ,
263- )
264-
265- const foreignRelationsToDisassociate = oldForeignRecords . flatMap (
266- ( record ) => this . getRelationsToOwner ( record ) ,
267- )
268-
269- // Throw if attempting to disassociate unique relations.
270- if ( this . options . unique ) {
271- invariant . as (
272- RelationError . for (
273- RelationErrorCodes . FORBIDDEN_UNIQUE_UPDATE ,
274- this . #createErrorDetails( ) ,
275- ) ,
276- foreignRelationsToDisassociate . length === 0 ,
277- 'Failed to update a unique relation at "%s": the foreign record is already associated with another owner' ,
278- update . path . join ( '.' ) ,
279- )
280- }
281-
282- for ( const foreignRelation of foreignRelationsToDisassociate ) {
283- foreignRelation . foreignKeys . delete ( update . prevRecord [ kPrimaryKey ] )
270+ this . ownerCollection . hooks . on (
271+ 'update' ,
272+ ( event ) => {
273+ const update = event . data
274+
275+ if ( isEqual ( update . path , path ) && isRecord ( update . nextValue ) ) {
276+ /**
277+ * @note Listeners are attached per-record but fire for every owner update.
278+ * Skip events whose target record's relation isn't this instance.
279+ */
280+ if ( update . prevRecord [ kRelationMap ] . get ( serializedPath ) !== this ) {
281+ return
284282 }
285283
286- // Check any other owners associated with the same foreign record.
287- // This is important since unique relations are not always two-way.
288- if ( this . options . unique ) {
289- const otherOwnersAssociatedWithForeignRecord =
290- this . #getOtherOwnerForRecords( [ update . nextValue ] )
291-
292- invariant . as (
293- RelationError . for (
294- RelationErrorCodes . FORBIDDEN_UNIQUE_UPDATE ,
295- this . #createErrorDetails( ) ,
296- ) ,
297- otherOwnersAssociatedWithForeignRecord == null ,
298- 'Failed to update a unique relation at "%s": the foreign record is already associated with another owner' ,
299- update . path . join ( '.' ) ,
284+ event . preventDefault ( )
285+
286+ // If the owner relation is "one-of", multiple foreign records cannot own this record.
287+ // Disassociate the old foreign records from pointing to the owner record.
288+ if ( this instanceof One ) {
289+ const oldForeignRecords =
290+ this . foreignCollections . flatMap < RecordType > (
291+ ( foreignCollection ) => {
292+ return foreignCollection . findMany ( ( q ) => {
293+ return q . where ( ( record ) => {
294+ return this . foreignKeys . has ( record [ kPrimaryKey ] )
295+ } )
296+ } )
297+ } ,
298+ )
299+
300+ const foreignRelationsToDisassociate = oldForeignRecords . flatMap (
301+ ( record ) => this . getRelationsToOwner ( record ) ,
300302 )
301- }
302303
303- this . foreignKeys . clear ( )
304- }
304+ // Throw if attempting to disassociate unique relations.
305+ if ( this . options . unique ) {
306+ invariant . as (
307+ RelationError . for (
308+ RelationErrorCodes . FORBIDDEN_UNIQUE_UPDATE ,
309+ this . #createErrorDetails( ) ,
310+ ) ,
311+ foreignRelationsToDisassociate . length === 0 ,
312+ 'Failed to update a unique relation at "%s": the foreign record is already associated with another owner' ,
313+ update . path . join ( '.' ) ,
314+ )
315+ }
316+
317+ for ( const foreignRelation of foreignRelationsToDisassociate ) {
318+ foreignRelation . foreignKeys . delete ( update . prevRecord [ kPrimaryKey ] )
319+ }
320+
321+ // Check any other owners associated with the same foreign record.
322+ // This is important since unique relations are not always two-way.
323+ if ( this . options . unique ) {
324+ const otherOwnersAssociatedWithForeignRecord =
325+ this . #getOtherOwnerForRecords( [ update . nextValue ] )
326+
327+ invariant . as (
328+ RelationError . for (
329+ RelationErrorCodes . FORBIDDEN_UNIQUE_UPDATE ,
330+ this . #createErrorDetails( ) ,
331+ ) ,
332+ otherOwnersAssociatedWithForeignRecord == null ,
333+ 'Failed to update a unique relation at "%s": the foreign record is already associated with another owner' ,
334+ update . path . join ( '.' ) ,
335+ )
336+ }
337+
338+ this . foreignKeys . clear ( )
339+ }
305340
306- // Associate the owner with a foreign record from the update data.
307- const foreignRecord = update . nextValue
308- this . foreignKeys . add ( foreignRecord [ kPrimaryKey ] )
341+ // Associate the owner with a foreign record from the update data.
342+ const foreignRecord = update . nextValue
343+ this . foreignKeys . add ( foreignRecord [ kPrimaryKey ] )
309344
310- for ( const foreignRelation of this . getRelationsToOwner ( foreignRecord ) ) {
311- foreignRelation . foreignKeys . add ( update . prevRecord [ kPrimaryKey ] )
345+ for ( const foreignRelation of this . getRelationsToOwner (
346+ foreignRecord ,
347+ ) ) {
348+ foreignRelation . foreignKeys . add ( update . prevRecord [ kPrimaryKey ] )
349+ }
312350 }
313- }
314- } )
351+ } ,
352+ { signal : abortController . signal } ,
353+ )
315354 }
316355
317356 public abstract resolve ( foreignKeys : Set < string > ) : unknown
0 commit comments