Skip to content

Commit 371619b

Browse files
committed
Apply diff from mswjs#350
1 parent d3656d8 commit 371619b

2 files changed

Lines changed: 239 additions & 3 deletions

File tree

src/relation.ts

Lines changed: 167 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,10 @@ export abstract class Relation {
196196
const update = event.data
197197

198198
if (
199+
update.path.length > path.length &&
199200
path.every((key, index) => key === update.path[index]) &&
200-
!isRecord(update.nextValue)
201+
!isRecord(update.nextValue) &&
202+
typeof update.path[path.length] !== 'number'
201203
) {
202204
event.preventDefault()
203205
event.stopImmediatePropagation()
@@ -230,6 +232,156 @@ export abstract class Relation {
230232
this.ownerCollection.hooks.on('update', (event) => {
231233
const update = event.data
232234

235+
if (this instanceof Many && isEqual(update.path, path)) {
236+
if (Array.isArray(update.nextValue)) {
237+
event.preventDefault()
238+
239+
const nextForeignRecords = update.nextValue
240+
241+
const otherOwnersAssociatedWithForeignRecord =
242+
this.#getOtherOwnerForRecords(nextForeignRecords)
243+
244+
invariant.as(
245+
RelationError.for(
246+
RelationErrorCodes.FORBIDDEN_UNIQUE_UPDATE,
247+
this.#createErrorDetails(),
248+
),
249+
this.options.unique ? otherOwnersAssociatedWithForeignRecord == null : true,
250+
'Failed to update a unique relation at "%s": the foreign record is already associated with another owner',
251+
update.path.join('.'),
252+
)
253+
254+
const nextForeignKeys = new Set<string>()
255+
for (const foreignRecord of nextForeignRecords) {
256+
invariant.as(
257+
RelationError.for(
258+
RelationErrorCodes.INVALID_FOREIGN_RECORD,
259+
this.#createErrorDetails(),
260+
),
261+
isRecord(foreignRecord),
262+
'Failed to update a relation at "%s": expected relational value to be a record but got "%j"',
263+
update.path.join('.'),
264+
foreignRecord,
265+
)
266+
267+
const foreignKey = foreignRecord[kPrimaryKey]
268+
invariant.as(
269+
RelationError.for(
270+
RelationErrorCodes.INVALID_FOREIGN_RECORD,
271+
this.#createErrorDetails(),
272+
),
273+
foreignKey != null,
274+
'Failed to update a relation at "%s": foreign record is missing primary key',
275+
update.path.join('.'),
276+
)
277+
278+
nextForeignKeys.add(foreignKey)
279+
}
280+
281+
// Remove associations that are no longer present.
282+
for (const foreignKey of this.foreignKeys) {
283+
if (nextForeignKeys.has(foreignKey)) {
284+
continue
285+
}
286+
287+
this.foreignKeys.delete(foreignKey)
288+
289+
for (const foreignCollection of this.foreignCollections) {
290+
const foreignRecord = foreignCollection.findFirst((q) =>
291+
q.where((record) => record[kPrimaryKey] === foreignKey),
292+
)
293+
294+
if (foreignRecord) {
295+
for (const foreignRelation of this.getRelationsToOwner(
296+
foreignRecord,
297+
)) {
298+
foreignRelation.foreignKeys.delete(
299+
update.prevRecord[kPrimaryKey],
300+
)
301+
}
302+
}
303+
}
304+
}
305+
306+
// Add new associations.
307+
for (const foreignRecord of nextForeignRecords) {
308+
const foreignKey = foreignRecord[kPrimaryKey]
309+
invariant.as(
310+
RelationError.for(
311+
RelationErrorCodes.INVALID_FOREIGN_RECORD,
312+
this.#createErrorDetails(),
313+
),
314+
foreignKey != null,
315+
'Failed to update a relation at "%s": foreign record is missing primary key',
316+
update.path.join('.'),
317+
)
318+
319+
if (foreignKey == null) {
320+
continue
321+
}
322+
323+
const isNewForeignKey = !this.foreignKeys.has(foreignKey)
324+
325+
if (isNewForeignKey) {
326+
this.foreignKeys.add(foreignKey)
327+
}
328+
329+
for (const foreignRelation of this.getRelationsToOwner(
330+
foreignRecord,
331+
)) {
332+
foreignRelation.foreignKeys.add(update.prevRecord[kPrimaryKey])
333+
}
334+
}
335+
}
336+
}
337+
338+
if (
339+
this instanceof Many &&
340+
path.length + 1 === update.path.length &&
341+
path.every((key, index) => key === update.path[index])
342+
) {
343+
const prevValue = update.prevValue
344+
const nextValue = update.nextValue
345+
346+
const prevForeignRecord = isRecord(prevValue) ? prevValue : undefined
347+
const nextForeignRecord = isRecord(nextValue) ? nextValue : undefined
348+
349+
if (prevForeignRecord) {
350+
this.foreignKeys.delete(prevForeignRecord[kPrimaryKey])
351+
352+
for (const foreignRelation of this.getRelationsToOwner(
353+
prevForeignRecord,
354+
)) {
355+
foreignRelation.foreignKeys.delete(update.prevRecord[kPrimaryKey])
356+
}
357+
}
358+
359+
if (nextForeignRecord) {
360+
const otherOwnersAssociatedWithForeignRecord =
361+
this.options.unique
362+
? this.#getOtherOwnerForRecords([nextForeignRecord])
363+
: undefined
364+
365+
invariant.as(
366+
RelationError.for(
367+
RelationErrorCodes.FORBIDDEN_UNIQUE_UPDATE,
368+
this.#createErrorDetails(),
369+
),
370+
this.options.unique ? otherOwnersAssociatedWithForeignRecord == null : true,
371+
'Failed to update a unique relation at "%s": the foreign record is already associated with another owner',
372+
update.path.join('.'),
373+
)
374+
375+
this.foreignKeys.add(nextForeignRecord[kPrimaryKey])
376+
377+
for (const foreignRelation of this.getRelationsToOwner(
378+
nextForeignRecord,
379+
)) {
380+
foreignRelation.foreignKeys.add(update.prevRecord[kPrimaryKey])
381+
}
382+
}
383+
}
384+
233385
if (isEqual(update.path, path) && isRecord(update.nextValue)) {
234386
event.preventDefault()
235387

@@ -524,21 +676,33 @@ class One extends Relation {
524676
}
525677

526678
export class Many extends Relation {
679+
private resolvedCache?: Array<RecordType>
680+
527681
public resolve(foreignKeys: Set<string>): unknown {
528682
if (foreignKeys.size === 0) {
683+
this.resolvedCache ??= []
684+
this.resolvedCache.length = 0
529685
return
530686
}
531687

532-
return this.foreignCollections.flatMap<RecordType>((foreignCollection) => {
688+
const resolved = this.foreignCollections.flatMap<RecordType>((foreignCollection) => {
533689
return foreignCollection.findMany((q) =>
534690
q.where((record) => {
535691
return foreignKeys.has(record[kPrimaryKey])
536692
}),
537693
)
538694
})
695+
696+
this.resolvedCache ??= []
697+
this.resolvedCache.length = 0
698+
this.resolvedCache.push(...resolved)
699+
700+
return this.resolvedCache
539701
}
540702

541703
public getDefaultValue(): unknown {
542-
return []
704+
this.resolvedCache ??= []
705+
this.resolvedCache.length = 0
706+
return this.resolvedCache
543707
}
544708
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Collection } from '#/src/collection.js'
2+
import z from 'zod'
3+
4+
const commentSchema = z.object({
5+
text: z.string(),
6+
})
7+
8+
const postSchema = z.object({
9+
get comments() {
10+
return z.array(commentSchema)
11+
},
12+
})
13+
14+
describe('many relations updates', () => {
15+
it('supports updating many relation via reassignment', async () => {
16+
const posts = new Collection({ schema: postSchema })
17+
const comments = new Collection({ schema: commentSchema })
18+
19+
posts.defineRelations(({ many }) => ({
20+
comments: many(comments),
21+
}))
22+
23+
const firstComment = await comments.create({ text: 'First' })
24+
const secondComment = await comments.create({ text: 'Second' })
25+
26+
const post = await posts.create({ comments: [firstComment] })
27+
28+
const updatedPost = await posts.update(
29+
post,
30+
{
31+
data(draft) {
32+
draft.comments = [...draft.comments, secondComment]
33+
},
34+
strict: true,
35+
},
36+
)
37+
38+
expect(updatedPost.comments.map((comment) => comment.text)).toEqual([
39+
'First',
40+
'Second',
41+
])
42+
})
43+
44+
it('supports updating many relation via push', async () => {
45+
const posts = new Collection({ schema: postSchema })
46+
const comments = new Collection({ schema: commentSchema })
47+
48+
posts.defineRelations(({ many }) => ({
49+
comments: many(comments),
50+
}))
51+
52+
const firstComment = await comments.create({ text: 'First' })
53+
const secondComment = await comments.create({ text: 'Second' })
54+
55+
const post = await posts.create({ comments: [firstComment] })
56+
57+
const updatedPost = await posts.update(
58+
post,
59+
{
60+
data(draft) {
61+
draft.comments.push(secondComment)
62+
},
63+
strict: true,
64+
},
65+
)
66+
67+
expect(updatedPost.comments.map((comment) => comment.text)).toEqual([
68+
'First',
69+
'Second',
70+
])
71+
})
72+
})

0 commit comments

Comments
 (0)