diff --git a/src/util/deserialize.ts b/src/util/deserialize.ts index 1e13698..a941130 100644 --- a/src/util/deserialize.ts +++ b/src/util/deserialize.ts @@ -100,11 +100,14 @@ class Deserializer { record: SpraypaintBase ): void { const modelIdx = model as any - const associationRecords = modelIdx[associationName] + const associationRecords = modelIdx[associationName] || [] const existingInstance = this.lookupAssociated(associationRecords, record) - if (!existingInstance) { + if (existingInstance) return + if (Array.isArray(modelIdx[associationName])) { modelIdx[associationName].push(record) + } else { + modelIdx[associationName] = record } } @@ -199,7 +202,7 @@ class Deserializer { if (Array.isArray(relationData)) { for (const datum of relationData) { const hydratedDatum = this.findResource(datum) - const associationRecords = instanceIdx[relationName] + const associationRecords = instanceIdx[relationName] || [] let relatedInstance = this.relationshipInstanceFor( hydratedDatum, associationRecords diff --git a/test/fixtures.ts b/test/fixtures.ts index f04ee85..43eb04a 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -119,6 +119,8 @@ export class Genre extends ApplicationRecord { @Attr name!: string @HasMany("authors") authors: any + + @BelongsTo({ type: "genres" }) parentGenre?: Genre } @Model() diff --git a/test/integration/relations.test.ts b/test/integration/relations.test.ts index 1d5cace..8c2af06 100644 --- a/test/integration/relations.test.ts +++ b/test/integration/relations.test.ts @@ -1,5 +1,5 @@ import { expect, fetchMock } from "../test-helper" -import { Author, NonFictionAuthor } from "../fixtures" +import { Author, Book, Genre, NonFictionAuthor } from "../fixtures" import { IResultProxy } from "../../src/proxies/index" import { SpraypaintBase } from "../../src/index" @@ -55,6 +55,54 @@ const generateMockResponse = (type: string) => { } as any } +const generateMockGenreResponse = () => { + return { + data: { + id: "1", + type: "books", + attributes: { + title: "A Song of Ice and Fire" + }, + relationships: { + genre: { + data: [ + { + id: "genre1", + type: "genres" + } + ] + } + } + }, + included: [ + { + id: "genre1", + type: "genres", + attributes: { + title: "Sword and Sourcery" + }, + relationships: { + parentGenre: { + data: [ + { + id: "genre2", + type: "genres" + } + ] + } + } + }, + { + id: "genre2", + type: "genres", + attributes: { + title: "Fantasy" + } + } + ] + } as any +} + describe("Relations", () => { describe("#find()", () => { beforeEach(() => { @@ -98,6 +146,23 @@ describe("Relations", () => { expect(data.genre).to.eq(undefined) }) }) + + describe("when there's a self-referential relationship", () => { + beforeEach(() => { + const response = generateMockGenreResponse() + fetchMock.get( + "http://example.com/api/books/1?include=genre.parent_genre", + response + ) + }) + + it("does not blow up", async () => { + const { data } = await Book.includes({ genre: "parentGenre" }).find(1) + expect(data.klass).to.eq(Book) + expect(data.genre).to.be.instanceOf(Genre) + expect(data.genre.parentGenre).to.be.instanceOf(Genre) + }) + }) }) describe("when keyCase is snake_case", () => { diff --git a/test/unit/model-relationships.test.ts b/test/unit/model-relationships.test.ts index e26bb59..7747a15 100644 --- a/test/unit/model-relationships.test.ts +++ b/test/unit/model-relationships.test.ts @@ -41,4 +41,16 @@ describe("Model relationships", () => { expect(keys).to.include("genre") }) + + it("supports self-referential relationships", () => { + const genre = new Genre({ name: "Fantasy" }) + const subgenre = new Genre({ + name: "Sword and Sourcery", + parentGenre: genre + }) + + expect(genre).to.be.instanceof(Genre) + expect(subgenre).to.be.instanceof(Genre) + expect(subgenre.parentGenre).to.be.instanceof(Genre) + }) })