Skip to content

RESTful API Handler to include many-to-many relations #2264

@stevendonorlink

Description

@stevendonorlink

Description and expected behavior
I am currently using the zenstack Restful API Handler with calling the following endpoint:
http://localhost:4001/api/user?include=offices

Below are my models (I have removed some fields to not overcomplicate things):

model User extends Base {
    externalId                   String
    firstName                    String                @length(1, 256)
    lastName                     String                @length(1, 256)
    email                        String
    username                     String
    offices                      UserOffice[]
}


model UserOffice extends NoIdBase {
    user         User    @relation(fields: [userId], references: [id], onDelete: Restrict)
    userId       String  @db.Uuid
    office       Office  @relation(fields: [officeId], references: [id], onDelete: Restrict)
    officeId     String  @db.Uuid

    @@unique([userId, officeId])
}

model Office extends Base {
    name                         String
    users                        UserOffice[]
    country                      String           @default('Australia')
    state                        String           @default('Victoria')
}

What I have received is as follow (I have removed some fields to not overcomplicate things):

{
    "jsonapi": {
        "version": "1.1"
    },
    "links": {
        "self": "http://localhost:4001/api/user",
        "first": "http://localhost:4001/api/user?filter%5Bid%5D=ae3ae174-d4fe-4869-8218-6a020d6878c5%2C68fb45cf-abac-4894-b6e0-2342911333fc%2C75908cc4-be29-4e1a-868a-3c12efec78be%2Cef95f21b-3e92-4044-a988-65dbd0a88b1b&page%5Blimit%5D=100",
        "last": "http://localhost:4001/api/user?filter%5Bid%5D=ae3ae174-d4fe-4869-8218-6a020d6878c5%2C68fb45cf-abac-4894-b6e0-2342911333fc%2C75908cc4-be29-4e1a-868a-3c12efec78be%2Cef95f21b-3e92-4044-a988-65dbd0a88b1b&page%5Boffset%5D=0",
        "prev": null,
        "next": null
    },
    "data": [
        {
            "type": "user",
            "id": "68fb45cf-abac-4894-b6e0-2342911333fc",
            "attributes": {
                "clientId": "9435cf4c-734c-4ae4-9636-6180b9b1bb41",
                "createdAt": "2025-09-23T03:18:36.229Z",
                "createdBy": "ae3ae174-d4fe-4869-8218-6a020d6878c5",
                "updatedAt": "2025-09-30T01:26:58.867Z",
                "updatedBy": "ae3ae174-d4fe-4869-8218-6a020d6878c5",
                "firstName": "Lauren",
                "lastName": "Admin1",
                "email": "dev+test+fundraiser1@test.com.au",
                "username": "dev+test+fundraiser1@test.com.au",
                "mobile": "0434511817",
                "status": "Active",
            },
            "links": {
                "self": "http://localhost:4001/api/user/68fb45cf-abac-4894-b6e0-2342911333fc"
            },
            "relationships": {
                "offices": {
                    "links": {
                        "self": "http://localhost:4001/api/user/68fb45cf-abac-4894-b6e0-2342911333fc/relationships/offices",
                        "related": "http://localhost:4001/api/user/68fb45cf-abac-4894-b6e0-2342911333fc/offices"
                    },
                    "data": [
                        {
                            "type": "userOffice",
                            "id": "68fb45cf-abac-4894-b6e0-2342911333fc_e282dcd6-ee66-4fe2-8e73-718b2c1b6866"
                        }
                    ]
                },
            }
        },
        {
            "type": "user",
            "id": "75908cc4-be29-4e1a-868a-3c12efec78be",
            "attributes": {
                "clientId": "9435cf4c-734c-4ae4-9636-6180b9b1bb41",
                "createdAt": "2025-09-30T06:15:25.248Z",
                "createdBy": "ae3ae174-d4fe-4869-8218-6a020d6878c5",
                "updatedAt": "2025-09-30T06:15:25.248Z",
                "updatedBy": null,
                "externalId": "993e7488-d061-703b-4102-a9d07a8e1680",
                "firstName": "Steven",
                "lastName": "4",
                "email": "steven+fund4@test.com.au",
                "username": "steven+fund4@test.com.au",
                "mobile": "0434511817",
                "status": "Inactive",
            },
            "links": {
                "self": "http://localhost:4001/api/user/75908cc4-be29-4e1a-868a-3c12efec78be"
            },
            "relationships": {
                "offices": {
                    "links": {
                        "self": "http://localhost:4001/api/user/75908cc4-be29-4e1a-868a-3c12efec78be/relationships/offices",
                        "related": "http://localhost:4001/api/user/75908cc4-be29-4e1a-868a-3c12efec78be/offices"
                    },
                    "data": [
                        {
                            "type": "userOffice",
                            "id": "75908cc4-be29-4e1a-868a-3c12efec78be_94f745fe-e145-49cc-a556-8a578c21962f"
                        },
                        {
                            "type": "userOffice",
                            "id": "75908cc4-be29-4e1a-868a-3c12efec78be_e282dcd6-ee66-4fe2-8e73-718b2c1b6866"
                        }
                    ]
                },
            }
        },
        {
            "type": "user",
            "id": "ef95f21b-3e92-4044-a988-65dbd0a88b1b",
            "attributes": {
                "clientId": "9435cf4c-734c-4ae4-9636-6180b9b1bb41",
                "createdAt": "2025-09-24T05:41:14.067Z",
                "createdBy": "ae3ae174-d4fe-4869-8218-6a020d6878c5",
                "updatedAt": "2025-10-01T05:44:32.206Z",
                "updatedBy": "ae3ae174-d4fe-4869-8218-6a020d6878c5",
                "firstName": "Steven123",
                "lastName": "Admin1",
                "email": "steven+fund10@ test.com.au",
                "username": "steven+fund10@test.com.au",
                "mobile": "0434511817",
                "status": "Active",
            },
            "links": {
                "self": "http://localhost:4001/api/user/ef95f21b-3e92-4044-a988-65dbd0a88b1b"
            },
            "relationships": {
                "offices": {
                    "links": {
                        "self": "http://localhost:4001/api/user/ef95f21b-3e92-4044-a988-65dbd0a88b1b/relationships/offices",
                        "related": "http://localhost:4001/api/user/ef95f21b-3e92-4044-a988-65dbd0a88b1b/offices"
                    },
                    "data": [
                        {
                            "type": "userOffice",
                            "id": "ef95f21b-3e92-4044-a988-65dbd0a88b1b_e282dcd6-ee66-4fe2-8e73-718b2c1b6866"
                        },
                        {
                            "type": "userOffice",
                            "id": "ef95f21b-3e92-4044-a988-65dbd0a88b1b_94f745fe-e145-49cc-a556-8a578c21962f"
                        }
                    ]
                },
            }
        }
    ],
    "included": [
        {
            "type": "userOffice",
            "id": "68fb45cf-abac-4894-b6e0-2342911333fc_e282dcd6-ee66-4fe2-8e73-718b2c1b6866",
            "attributes": {
                "clientId": "9435cf4c-734c-4ae4-9636-6180b9b1bb41",
                "createdAt": "2025-09-23T03:18:36.233Z",
                "createdBy": "ae3ae174-d4fe-4869-8218-6a020d6878c5",
                "updatedAt": "2025-09-23T03:18:36.233Z",
                "updatedBy": null,
                "userId": "68fb45cf-abac-4894-b6e0-2342911333fc",
                "officeId": "e282dcd6-ee66-4fe2-8e73-718b2c1b6866",
                "isHomeOffice": true
            },
            "links": {
                "self": "http://localhost:4001/api/userOffice/68fb45cf-abac-4894-b6e0-2342911333fc_e282dcd6-ee66-4fe2-8e73-718b2c1b6866"
            },
            "relationships": {
                "user": {
                    "links": {
                        "self": "http://localhost:4001/api/userOffice/68fb45cf-abac-4894-b6e0-2342911333fc_e282dcd6-ee66-4fe2-8e73-718b2c1b6866/relationships/user",
                        "related": "http://localhost:4001/api/userOffice/68fb45cf-abac-4894-b6e0-2342911333fc_e282dcd6-ee66-4fe2-8e73-718b2c1b6866/user"
                    }
                },
                "office": {
                    "links": {
                        "self": "http://localhost:4001/api/userOffice/68fb45cf-abac-4894-b6e0-2342911333fc_e282dcd6-ee66-4fe2-8e73-718b2c1b6866/relationships/office",
                        "related": "http://localhost:4001/api/userOffice/68fb45cf-abac-4894-b6e0-2342911333fc_e282dcd6-ee66-4fe2-8e73-718b2c1b6866/office"
                    }
                }
            }
        }
    ],
    "meta": {
        "serialization": {
            "values": {
                "data.0.attributes.createdAt": [
                    "Date"
                ],
                "data.0.attributes.updatedAt": [
                    "Date"
                ],
                "data.1.attributes.createdAt": [
                    "Date"
                ],
                "data.1.attributes.updatedAt": [
                    "Date"
                ],
                "data.2.attributes.createdAt": [
                    "Date"
                ],
                "data.2.attributes.updatedAt": [
                    "Date"
                ],
                "data.3.attributes.createdAt": [
                    "Date"
                ],
                "data.3.attributes.updatedAt": [
                    "Date"
                ],
                "included.0.attributes.createdAt": [
                    "Date"
                ],
                "included.0.attributes.updatedAt": [
                    "Date"
                ]
            }
        },
        "total": 3
    }
}

As you can see I want to include the many-to-many relations (in our case offices/userOffice). The data we have received back shows all the relationships (with just the Ids). However, the included part is only returning 1 single userOffice even though there should be multiple (2 offices in our case).

When I include One-to-Many relationships, for example one user have one level, (or one level have many user) it would then show the multiple level based on the users returned, which is good.

The problem here is that when it is many-to-many relationship, it is not returning all the relations. Rather, it only return one (from what I can see, which is the first user's userOffice).

Please any assistant would be great, otherwise, it would be painful to keep calling multiple endpoints to list down all the relationships.

Environment:

  • ZenStack version: 2.19.1
  • Prisma version: 6.16.2
  • Database type: Postgresql

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions