data icon indicating copy to clipboard operation
data copied to clipboard

v1.1.2: "One-to-many (inversed)" not working as documented

Open heiwen opened this issue 1 month ago • 6 comments

I'm trying to reproduce the following documented example:

import { Collection } from "@msw/data"
import z from "zod/v4"

const postSchema = z.object({
  get comments() {
    return z.array(commentSchema)
  },
})
const commentSchema = z.object({
  text: z.string(),
  get post() {
    return postSchema
  },
})

const posts = new Collection({ schema: postSchema })
const comments = new Collection({ schema: commentSchema })

posts.defineRelations(({ many }) => ({
  comments: many(comments),
}))
comments.defineRelations(({ one }) => ({
  post: one(posts),
}))

await posts.create({
  comments: [await comments.create({ text: 'First!' })],
})

const comment = comments.findFirst((q) => q.where({ text: 'First!' }))
comments.post // { comments: [{ text: 'First', post: Circular }] }

It fails with the error message:

error: Failed to create a new record with initial values: does not match the schema. Please see the schema validation errors above.

I'm using Bun 1.2.0, msw 2.12.2, msw/data 1.1.2 and zod 4.1.12.

heiwen avatar Nov 17 '25 15:11 heiwen

Hi, @heiwen. Thanks for pointing this out.

Since self-referencing non-optional records are currently impossible (see #318), the example has to be updated to create post through comments (comments can have an empty array as initial value, which won't violate the schema).

This is what you should do instead:

await comments.create({
  text: 'First!',
  post: await posts.create({ comments: [] })
})

Despite setting an empty comments upon creating the post, Data will push the created comment to that post's comments automatically. The empty array is there to align with the schema that doesn't mark either comments or post as optional.

Could you please try this and let me know if this works in your case? Thanks.

kettanaito avatar Nov 17 '25 16:11 kettanaito

Thank you for the quick reply, that works! :)

I'm also able to add a second post by doing the following:

const comment = comments.findFirst((q) => q.where({ text: 'First!' }))
const post = comment!.post // { comments: [{ text: 'First', post: Circular }] }

await comments.create({
  text: 'Second!',
  post: post
})

BUT, when returning the post or comment in msw using a HttpResponse.json, I now get the following error message:

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    |     property 'comments' -> object with constructor 'Array'
    |     index 0 -> object with constructor 'Object'
    --- property 'post' closes the circle
   at JSON.stringify (<anonymous>)

I understand the nature of the error.

Just wondering whether there is a recommended approach / is there a convenient way to remove the circular dependency when returning the data in the API mock without having to write a lot of boilerplate in every route?

heiwen avatar Nov 18 '25 03:11 heiwen

Circular objects are hard. For JSON responses, I wonder if there's an unopinionated solution we can present. It is really up to you how you structure your models, but it would be great if the library helped with situations like this.

What kind of solution do you see here?

This reminds me of circular graphs in GraphQL, where they solve it by never exposing you the data you haven't explicitly requested. That's not a thing with RESTful APIs that usually return complete resources.

On a technical level, someone has to handle the serialization of such circular objects at some point. But this is really a design question more than anything. Where should such circular references short-circuit? How would the user control that?

kettanaito avatar Nov 18 '25 09:11 kettanaito

I'd probably ask: "How do other ORMs like Prisma / Drizzle" handle it?

I think in their schema, one side of the relation is always defined as the default. In a query itself, you can query both sides But when data is returned, only one side is attached, avoiding circular dependencies.

Is that conceptually a solution for msw data as well?

heiwen avatar Nov 18 '25 10:11 heiwen

Would you be interested in help me with finding concrete examples from those libraries and presenting them here?

Data is open to those solutions since it's modeled after libraries like Drizzle and Prisma. So, yes, we should learn from them and come up with an approach to help everyone in this situation. I'd be grateful for teaming up with you on this one.

kettanaito avatar Nov 18 '25 10:11 kettanaito

Here is what I see from Prisma (which we use for our real API):

It seems to be purely query based. Meaning, any property that represents a relation needs to be explicitly defined in the query so that it is included in the returned object (using an include parameter in the query):

https://www.prisma.io/docs/orm/prisma-client/queries/relation-queries#include-a-relation

This is also true for create operations (which return the original object):

https://www.prisma.io/docs/orm/prisma-client/queries/relation-queries#create-a-related-record

By nature of that explicit "include" definition, circular structures are avoided.

heiwen avatar Nov 18 '25 11:11 heiwen