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
21 changes: 20 additions & 1 deletion src/relation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,8 +617,27 @@ export abstract class Relation {
*/
public getRelationsToOwner(foreignRecord: RecordType): Array<Relation> {
const result: Array<Relation> = []
const isSelfReferencing = this.foreignCollections.some(
(foreignCollection) => {
return (
foreignCollection[kCollectionId] ===
this.ownerCollection[kCollectionId]
)
},
)
const ownPath = this.path.join('.')

for (const [serializedPath, relation] of foreignRecord[kRelationMap]) {
/**
* @note For self-referencing relations, the relation at the same path
* on the foreign record is not the inverse — it's the same logical
* relation pointing in the same direction. Skip it so we only return
* the actual inverse relation (a different path with the same role).
*/
if (isSelfReferencing && serializedPath === ownPath) {
continue
}

for (const [, relation] of foreignRecord[kRelationMap]) {
if (
relation.foreignCollections.some((foreignCollection) => {
return (
Expand Down
118 changes: 118 additions & 0 deletions tests/relations/many-to-many.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,121 @@ it('scopes a nested many-to-many relation update to the targeted record', async

expect(posts.all().map((post) => post.title)).toEqual(['Updated', 'Second'])
})

it('creates a self referencing many-to-many relation', async () => {
const userSchema = z.object({
id: z.number(),
get children() {
return z.array(userSchema).optional()
},
get parents() {
return z.array(userSchema).optional()
},
})

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

users.defineRelations(({ many }) => ({
children: many(users, { role: 'hierarchy' }),
parents: many(users, { role: 'hierarchy' }),
}))

{
const childOne = await users.create({
id: 1,
})

const parentOne = await users.create({
id: 2,
children: [childOne],
})

expect
.soft(parentOne.children)
.toEqual([expect.objectContaining({ id: 1 })])
expect.soft(parentOne.parents).toEqual([])

expect.soft(childOne.children).toEqual([])
expect.soft(childOne.parents).toEqual([expect.objectContaining({ id: 2 })])
}

{
const parentTwo = await users.create({ id: 3 })
const childTwo = await users.create({ id: 4, parents: [parentTwo] })

expect
.soft(parentTwo.children)
.toEqual([expect.objectContaining({ id: 4 })])
expect.soft(parentTwo.parents).toEqual([])

expect.soft(childTwo.children).toEqual([])
expect.soft(childTwo.parents).toEqual([expect.objectContaining({ id: 3 })])
}
})

it('updates a self referencing many-to-many relation', async () => {
const userSchema = z.object({
id: z.number(),
get children() {
return z.array(userSchema).optional()
},
get parents() {
return z.array(userSchema).optional()
},
})

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

users.defineRelations(({ many }) => ({
children: many(users, { role: 'hierarchy' }),
parents: many(users, { role: 'hierarchy' }),
}))

const childOne = await users.create({ id: 1 })
const childTwo = await users.create({ id: 2 })
const parent = await users.create({ id: 3, children: [childOne] })

await users.update(parent, {
data(draft) {
draft.children = [childTwo]
},
})

expect.soft(parent.children).toEqual([expect.objectContaining({ id: 2 })])
expect.soft(parent.parents).toEqual([])
expect.soft(childOne.parents).toEqual([])
expect.soft(childTwo.parents).toEqual([expect.objectContaining({ id: 3 })])
})

it('resolves a cyclic self referencing many-to-many relation', async () => {
const userSchema = z.object({
id: z.number(),
get children() {
return z.array(userSchema).optional()
},
get parents() {
return z.array(userSchema).optional()
},
})

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

users.defineRelations(({ many }) => ({
children: many(users, { role: 'hierarchy' }),
parents: many(users, { role: 'hierarchy' }),
}))

const alice = await users.create({ id: 1 })
const bob = await users.create({ id: 2, parents: [alice] })

await users.update(alice, {
data(draft) {
draft.parents = [bob]
},
})

expect.soft(alice.parents).toEqual([expect.objectContaining({ id: 2 })])
expect.soft(alice.children).toEqual([expect.objectContaining({ id: 2 })])
expect.soft(bob.parents).toEqual([expect.objectContaining({ id: 1 })])
expect.soft(bob.children).toEqual([expect.objectContaining({ id: 1 })])
})
67 changes: 67 additions & 0 deletions tests/relations/many-to-one.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,70 @@ it("scopes nested updates to the updated owner's foreign record", async () => {

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

it('creates a self referencing many-to-one relation', async () => {
const userSchema = z.object({
id: z.number(),
get children() {
return z.array(userSchema).optional()
},
get parent() {
return userSchema.optional()
},
})

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

users.defineRelations(({ one, many }) => ({
children: many(users, { role: 'hierarchy' }),
parent: one(users, { role: 'hierarchy' }),
}))

const parent = await users.create({
id: 2,
})

const child = await users.create({
id: 1,
parent,
})

expect.soft(parent.children).toEqual([expect.objectContaining({ id: 1 })])
expect.soft(parent.parent).toBeUndefined()

expect.soft(child.children).toEqual([])
expect.soft(child.parent).toEqual(expect.objectContaining({ id: 2 }))
})

it('updates a self referencing many-to-one relation', async () => {
const userSchema = z.object({
id: z.number(),
get children() {
return z.array(userSchema).optional()
},
get parent() {
return userSchema.optional()
},
})

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

users.defineRelations(({ one, many }) => ({
children: many(users, { role: 'hierarchy' }),
parent: one(users, { role: 'hierarchy' }),
}))

const parentOne = await users.create({ id: 1 })
const parentTwo = await users.create({ id: 2 })
const child = await users.create({ id: 3, parent: parentOne })

await users.update(child, {
data(draft) {
draft.parent = parentTwo
},
})

expect.soft(child.parent).toEqual(expect.objectContaining({ id: 2 }))
expect.soft(parentOne.children).toEqual([])
expect.soft(parentTwo.children).toEqual([expect.objectContaining({ id: 3 })])
})
66 changes: 66 additions & 0 deletions tests/relations/one-to-many.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,3 +539,69 @@ it('scopes a nested one-to-many relation update to the targeted record', async (

expect(posts.all().map((post) => post.title)).toEqual(['Updated', 'Second'])
})

it('creates a self referencing one-to-many relation', async () => {
const userSchema = z.object({
id: z.number(),
get children() {
return z.array(userSchema).optional()
},
get parent() {
return userSchema.optional()
},
})

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

users.defineRelations(({ one, many }) => ({
children: many(users, { role: 'hierarchy' }),
parent: one(users, { role: 'hierarchy' }),
}))

const child = await users.create({
id: 1,
})
const parent = await users.create({
id: 2,
children: [child],
})

expect.soft(parent.children).toEqual([expect.objectContaining({ id: 1 })])
expect.soft(parent.parent).toBeUndefined()

expect.soft(child.children).toEqual([])
expect.soft(child.parent).toEqual(expect.objectContaining({ id: 2 }))
})

it('updates a self referencing one-to-many relation', async () => {
const userSchema = z.object({
id: z.number(),
get children() {
return z.array(userSchema).optional()
},
get parent() {
return userSchema.optional()
},
})

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

users.defineRelations(({ one, many }) => ({
children: many(users, { role: 'hierarchy' }),
parent: one(users, { role: 'hierarchy' }),
}))

const childOne = await users.create({ id: 1 })
const childTwo = await users.create({ id: 2 })
const parent = await users.create({ id: 3, children: [childOne] })

await users.update(parent, {
data(draft) {
draft.children = [childTwo]
},
})

expect.soft(parent.children).toEqual([expect.objectContaining({ id: 2 })])
expect.soft(childOne.parent).toBeUndefined()
expect.soft(childTwo.parent).toEqual(expect.objectContaining({ id: 3 }))
})
99 changes: 99 additions & 0 deletions tests/relations/one-to-one.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,3 +651,102 @@ it('removes relation listeners when the owner record is deleted', async () => {
'Detaches relation listeners when the owner record is deleted',
).toBe(baseline)
})

it('creates a self referencing one-to-one relation', async () => {
const userSchema = z.object({
id: z.number(),
get child() {
return userSchema.optional()
},
get parent() {
return userSchema.optional()
},
})

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

users.defineRelations(({ one }) => ({
child: one(users, { role: 'hierarchy' }),
parent: one(users, { role: 'hierarchy' }),
}))

const parent = await users.create({
id: 2,
})

const child = await users.create({
id: 1,
parent,
})

expect.soft(parent.child).toEqual(expect.objectContaining({ id: 1 }))
expect.soft(parent.parent).toBeUndefined()

expect.soft(child.child).toBeUndefined()
expect.soft(child.parent).toEqual(expect.objectContaining({ id: 2 }))
})

it('updates a self referencing one-to-one relation', async () => {
const userSchema = z.object({
id: z.number(),
get child() {
return userSchema.optional()
},
get parent() {
return userSchema.optional()
},
})

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

users.defineRelations(({ one }) => ({
child: one(users, { role: 'hierarchy' }),
parent: one(users, { role: 'hierarchy' }),
}))

const parentOne = await users.create({ id: 1 })
const parentTwo = await users.create({ id: 2 })
const child = await users.create({ id: 3, parent: parentOne })

await users.update(child, {
data(draft) {
draft.parent = parentTwo
},
})

expect.soft(child.parent).toEqual(expect.objectContaining({ id: 2 }))
expect.soft(parentOne.child).toBeUndefined()
expect.soft(parentTwo.child).toEqual(expect.objectContaining({ id: 3 }))
})

it('resolves a cyclic self referencing one-to-one relation', async () => {
const userSchema = z.object({
id: z.number(),
get child() {
return userSchema.optional()
},
get parent() {
return userSchema.optional()
},
})

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

users.defineRelations(({ one }) => ({
child: one(users, { role: 'hierarchy' }),
parent: one(users, { role: 'hierarchy' }),
}))

const alice = await users.create({ id: 1 })
const bob = await users.create({ id: 2, parent: alice })

await users.update(alice, {
data(draft) {
draft.parent = bob
},
})

expect.soft(alice.parent).toEqual(expect.objectContaining({ id: 2 }))
expect.soft(bob.parent).toEqual(expect.objectContaining({ id: 1 }))
expect.soft(alice.parent?.parent).toEqual(expect.objectContaining({ id: 1 }))
})
Loading