Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions src/relation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,14 @@ export abstract class Relation {
path.every((key, index) => key === update.path[index]) &&
!isRecord(update.nextValue)
) {
/**
* @note Listeners are attached per-record but fire for every owner update.
* Skip events whose target record's relation isn't this instance.
*/
if (update.prevRecord[kRelationMap].get(serializedPath) !== this) {
return
}

event.preventDefault()
event.stopImmediatePropagation()

Expand Down Expand Up @@ -231,6 +239,14 @@ export abstract class Relation {
const update = event.data

if (isEqual(update.path, path) && isRecord(update.nextValue)) {
/**
* @note Listeners are attached per-record but fire for every owner update.
* Skip events whose target record's relation isn't this instance.
*/
if (update.prevRecord[kRelationMap].get(serializedPath) !== this) {
return
}

event.preventDefault()

// If the owner relation is "one-of", multiple foreign records cannot own this record.
Expand Down Expand Up @@ -269,18 +285,20 @@ export abstract class Relation {

// Check any other owners associated with the same foreign record.
// This is important since unique relations are not always two-way.
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) {
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('.'),
)
}

this.foreignKeys.clear()
}
Expand Down
26 changes: 26 additions & 0 deletions tests/relations/many-to-many.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,29 @@ it('respects updates of foreign records', async () => {
expect(firstPost.authors).toEqual([firstUser, updatedSecondUser])
expect(secondPost.authors).toEqual([firstUser, updatedSecondUser])
})

it('scopes a nested many-to-many relation update to the targeted record', async () => {
const users = new Collection({ schema: userSchema })
const posts = new Collection({ schema: postSchema })

users.defineRelations(({ many }) => ({
posts: many(posts),
}))
posts.defineRelations(({ many }) => ({
authors: many(users),
}))

const firstPost = await posts.create({ title: 'First' })
const secondPost = await posts.create({ title: 'Second' })

const firstUser = await users.create({ id: 1, posts: [firstPost] })
await users.create({ id: 2, posts: [secondPost] })

await users.update(firstUser, {
data(user) {
user.posts[0]!.title = 'Updated'
},
})

expect(posts.all().map((post) => post.title)).toEqual(['Updated', 'Second'])
})
92 changes: 92 additions & 0 deletions tests/relations/many-to-one.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,95 @@ it('supports many-to-one relations', async () => {
.soft(posts.findFirst((q) => q.where({ title: 'Second' })))
.toEqual({ title: 'Second', author: { id: 1 } })
})

it('scopes a many-to-one relation update to the targeted record', async () => {
const users = new Collection({ schema: userSchema })
const posts = new Collection({ schema: postSchema })

posts.defineRelations(({ one }) => ({
author: one(users),
}))

const userOne = await users.create({ id: 1 })
const userTwo = await users.create({ id: 2 })

const firstPost = await posts.create({ title: 'First', author: userOne })
await posts.create({ title: 'Second', author: userTwo })

await posts.update(firstPost, {
data(post) {
post.author = userTwo
},
})
await posts.update(firstPost, {
data(post) {
post.author = userOne
},
})

expect(posts.all()).toEqual([
{ title: 'First', author: { id: 1 } },
{ title: 'Second', author: { id: 2 } },
])
})

it('updates a many-to-one relation to a foreign record already associated with another owner', async () => {
const languageSchema = z.object({ code: z.string() })
const userSchema = z.object({
name: z.string(),
language: languageSchema,
})

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

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

const langPt = await languages.create({ code: 'pt' })
const langEn = await languages.create({ code: 'en' })

const userOne = await users.create({ name: 'John', language: langPt })
await users.create({ name: 'Kate', language: langEn })

await users.update(userOne, {
data(user) {
user.language = langEn
},
})

expect(users.all()).toEqual([
{ name: 'John', language: { code: 'en' } },
{ name: 'Kate', language: { code: 'en' } },
])
})

it("scopes nested updates to the updated owner's foreign record", async () => {
const countrySchema = z.object({ code: z.string() })
const userSchema = z.object({
name: z.string(),
country: countrySchema,
})

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

users.defineRelations(({ one }) => ({
country: one(countries),
}))

const us = await countries.create({ code: 'us' })
const ca = await countries.create({ code: 'ca' })

const userOne = await users.create({ name: 'John', country: us })
await users.create({ name: 'Kate', country: ca })

await users.update(userOne, {
data(user) {
user.country.code = 'uk'
},
})

expect(countries.all()).toEqual([{ code: 'uk' }, { code: 'ca' }])
})
26 changes: 26 additions & 0 deletions tests/relations/one-to-many.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,3 +453,29 @@ it('errors when creating a unique relation with already associated foreign recor
)
}
})

it('scopes a nested one-to-many relation update to the targeted record', async () => {
const users = new Collection({ schema: userSchema })
const posts = new Collection({ schema: postSchema })

users.defineRelations(({ many }) => ({
posts: many(posts),
}))

const firstUser = await users.create({
id: 1,
posts: [await posts.create({ title: 'First' })],
})
await users.create({
id: 2,
posts: [await posts.create({ title: 'Second' })],
})

await users.update(firstUser, {
data(user) {
user.posts[0]!.title = 'Updated'
},
})

expect(posts.all().map((post) => post.title)).toEqual(['Updated', 'Second'])
})
55 changes: 55 additions & 0 deletions tests/relations/one-to-one.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,3 +562,58 @@ it('errors when updating a unique two-way relation referencing a taken foreign r
),
)
})

it('scopes a one-to-one relation update to the targeted record', async () => {
const users = new Collection({ schema: userSchema })
const countries = new Collection({ schema: countrySchema })

users.defineRelations(({ one }) => ({
country: one(countries),
}))

await users.create({
id: 1,
country: await countries.create({ code: 'us' }),
})
await users.create({
id: 2,
country: await countries.create({ code: 'ca' }),
})

await users.update((q) => q.where({ id: 1 }), {
async data(user) {
user.country = await countries.create({ code: 'uk' })
},
})

expect(users.all()).toEqual([
{ id: 1, country: { code: 'uk' } },
{ id: 2, country: { code: 'ca' } },
])
})

it('scopes a nested one-to-one relation update to the targeted record', async () => {
const users = new Collection({ schema: userSchema })
const countries = new Collection({ schema: countrySchema })

users.defineRelations(({ one }) => ({
country: one(countries),
}))

await users.create({
id: 1,
country: await countries.create({ code: 'us' }),
})
await users.create({
id: 2,
country: await countries.create({ code: 'ca' }),
})

await users.update((q) => q.where({ id: 1 }), {
data(user) {
user.country.code = 'uk'
},
})

expect(countries.all()).toEqual([{ code: 'uk' }, { code: 'ca' }])
})
Loading