maoosi/prisma-appsync

Issue(mongoDB): Types missing in generated GraphQL schema. IDs and relation scalar fields not supported.

mikerudge opened this issue · 7 comments

When using the type for embedded documents, it seems as though the schema that is generated doesnt include the type.

Here is a quick example. If we use this as the schema.

datasource db {
    provider = "mongodb"
    url      = env("DATABASE_URL")
}

generator appsync {
    provider = "prisma-appsync"
}

generator client {
    provider = "prisma-client-js"
}

type Address {
    street String
    city   String
    zip    String
}

type Photo {
    height Int
    width  Int
    url    String
}

/// @gql(subscriptions: null, mutations: null)
model User {
    id        String   @id @default(auto()) @map("_id") @db.ObjectId
    email     String   @unique
    name      String?
    photo     Photo?
    address   Address?
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
}

Then this is the output

type User {
    id: String!
    email: AWSEmail!
    name: String
    photo: Photo
    address: Address <--- type of Address is here but not defined anywhere? 
    createdAt: AWSDateTime!
    updatedAt: AWSDateTime!
}

type BatchPayload {
    count: Int
}

enum OrderByArg {
    ASC
    DESC
}

input UserFilter {
    some: UserWhereInput
    every: UserWhereInput
    none: UserWhereInput
}

input UserWhereInput {
    OR: [UserWhereInput]
    NOT: [UserWhereInput]
    AND: [UserWhereInput]
    id: StringFilter
    email: AWSEmailFilter
    name: StringFilter
    photo: StringFilter
    address: StringFilter <-- address is a string here, but it should be an object
    createdAt: AWSDateTimeFilter
    updatedAt: AWSDateTimeFilter
}

input UserWhereUniqueInput {
    id: String
    email: AWSEmail
}

input UserExtendedWhereUniqueInput {
    OR: [UserWhereInput]
    NOT: [UserWhereInput]
    AND: [UserWhereInput]
    id: String
    email: AWSEmail
    name: StringFilter
    photo: StringFilter
    address: StringFilter
    createdAt: AWSDateTimeFilter
    updatedAt: AWSDateTimeFilter
}

input UserOrderByInput {
    id: OrderByArg
    email: OrderByArg
    name: OrderByArg
    photo: OrderByArg
    address: OrderByArg
    createdAt: OrderByArg
    updatedAt: OrderByArg
}

input UserCreateInput {
    id: String!
    email: AWSEmail!
    name: String
    photo: Photo
    address: Address <--- address type here
    createdAt: AWSDateTime
    updatedAt: AWSDateTime
}

input UserCreateManyInput {
    id: String!
    email: AWSEmail!
    name: String
    photo: Photo
    address: Address <--- address type here
    createdAt: AWSDateTime
    updatedAt: AWSDateTime
}

input UserUpdateInput {
    id: String
    email: AWSEmail
    name: String
    photo: Photo
    address: Address <--- address type here
    createdAt: AWSDateTime
    updatedAt: AWSDateTime
}

input UserUpdateUniqueInput {
    data: UserUpdateInput!
    where: UserWhereUniqueInput!
}

input UserUpdateManyInput {
    where: UserWhereInput!
    data: UserUpdateInput!
}

input UserUpsertInput {
    create: UserCreateInput!
    update: UserUpdateInput!
}

input UserUpsertUniqueInput {
    where: UserWhereUniqueInput!
    create: UserCreateInput!
    update: UserUpdateInput!
}

input UserConnectOrCreateInput {
    where: UserWhereUniqueInput!
    create: UserCreateInput!
}

input UserDeleteUniqueInput {
    where: UserWhereUniqueInput!
}

input UserDeleteManyInput {
    where: UserWhereInput!
}

input IntOperation {
    set: Int
    increment: Int
    decrement: Int
    multiply: Int
    divide: Int
}

input FloatOperation {
    set: Float
    increment: Float
    decrement: Float
    multiply: Float
    divide: Float
}

input AWSDateTimeFilter {
    equals: AWSDateTime
    gt: AWSDateTime
    gte: AWSDateTime
    in: [AWSDateTime!]
    lt: AWSDateTime
    lte: AWSDateTime
    not: AWSDateTimeFilter
    notIn: [AWSDateTime!]
}

input AWSDateTimeListFilter {
    equals: [AWSDateTime!]
    has: AWSDateTime
    hasEvery: [AWSDateTime!]
    hasSome: [AWSDateTime!]
    isEmpty: Boolean
}

input FloatFilter {
    equals: Float
    gt: Float
    gte: Float
    in: [Float!]
    lt: Float
    lte: Float
    not: FloatFilter
    notIn: [Float!]
}

input FloatListFilter {
    equals: [Float!]
    has: Float
    hasEvery: [Float!]
    hasSome: [Float!]
    isEmpty: Boolean
}

input IntFilter {
    equals: Int
    gt: Int
    gte: Int
    in: [Int!]
    lt: Int
    lte: Int
    not: IntFilter
    notIn: [Int!]
}

input IntListFilter {
    equals: [Int!]
    has: Int
    hasEvery: [Int!]
    hasSome: [Int!]
    isEmpty: Boolean
}

input AWSJSONFilter {
    contains: String
    endsWith: String
    equals: AWSJSON
    in: [AWSJSON!]
    not: AWSJSONFilter
    notIn: [AWSJSON!]
    startsWith: String
}

input AWSJSONListFilter {
    equals: [AWSJSON!]
    has: AWSJSON
    hasEvery: [AWSJSON!]
    hasSome: [AWSJSON!]
    isEmpty: Boolean
}

input AWSEmailFilter {
    contains: String
    endsWith: String
    equals: AWSEmail
    in: [AWSEmail!]
    not: AWSEmailFilter
    notIn: [AWSEmail!]
    startsWith: String
}

input AWSEmailListFilter {
    equals: [AWSEmail!]
    has: AWSEmail
    hasEvery: [AWSEmail!]
    hasSome: [AWSEmail!]
    isEmpty: Boolean
}

input AWSURLFilter {
    contains: String
    endsWith: String
    equals: AWSURL
    in: [AWSURL!]
    not: AWSURLFilter
    notIn: [AWSURL!]
    startsWith: String
}

input AWSURLListFilter {
    equals: [AWSURL!]
    has: AWSURL
    hasEvery: [AWSURL!]
    hasSome: [AWSURL!]
    isEmpty: Boolean
}

input StringFilter {
    contains: String
    endsWith: String
    equals: String
    in: [String!]
    not: StringFilter
    notIn: [String!]
    startsWith: String
    mode: String
}

input StringListFilter {
    equals: [String!]
    has: String
    hasEvery: [String!]
    hasSome: [String!]
    isEmpty: Boolean
}

input BooleanFilter {
    equals: Boolean
    not: BooleanFilter
}

input BooleanListFilter {
    equals: [Boolean!]
    has: Boolean
    hasEvery: [Boolean!]
    hasSome: [Boolean!]
}

type Query {
    """
    Find a single User record by unique identifier.
    """
    getUser(where: UserWhereUniqueInput!): User

    """
    Find many User records (optional query filters).
    """
    listUsers(
        where: UserWhereInput
        orderBy: [UserOrderByInput]
        skip: Int
        take: Int
    ): [User]

    """
    Count all User records (optional query filters).
    """
    countUsers(
        where: UserWhereInput
        orderBy: [UserOrderByInput]
        skip: Int
        take: Int
    ): Int
}
maoosi commented

Hey @mikerudge, I am not familiar with MongoDB - but looking at your Prisma schema, I can see that both Address and Photo don't have id fields specified? This could explain why Prisma-AppSync is skipping these models.

I have found some Prisma documentation on the topic, but it is quite lite for people who aren't familiar with MongoDB. To fix the issue, I would probably need some time to play with MongoDB and see how we can best support IDs and relation scalar fields with the @map directive.

Is the same Prisma schema (with no id fields on Address and Photo) working ok with Prisma Client outside of Prisma-AppSync?

Hey

Yup it works fine with just prisma, in fact this example is from the prisms docs.

There is no need to have IDs on embedded documents,they are stored as just a json object on the document.There is no referencing or relationship needed.

Here is an example:

const newOrder = await prisma.order.create({
  data: {
    // Relation (via reference ID)
    product: { connect: { id: 'some-object-id' } },
    color: 'Red',
    // Embedded document
    shippingAddress: {
      street: '1084 Candycane Lane',
      city: 'Silverlake',
      zip: '84323',
    },
  },
})

I will see if I can manually update the generated schema to show what I would expect the output to be.

maoosi commented

I will see if I can manually update the generated schema to show what I would expect the output to be.

Yes please, that would definitely help!

Hopefully this helps.

So I added

type Address {
  street: String
  city: String
  state: String
  zip: String
}

type Photo {
  height: Int
  width: Int
  url: String
}

input AddressInput {
  street: String
  city: String
  state: String
  zip: String
}

input PhotoInput {
  height: Int
  width: Int
  url: String
}

Then I updated all the address fields and photo fields to use the inputs, except for the user type, which uses the regular Address type.

input UserWhereInput {
  OR: [UserWhereInput]
  NOT: [UserWhereInput]
  AND: [UserWhereInput]
  id: StringFilter
  email: AWSEmailFilter
  name: StringFilter
  photo: PhotoInput
  address: AddressInput
  createdAt: AWSDateTimeFilter
  updatedAt: AWSDateTimeFilter
}

type User {
  id: String!
  email: AWSEmail!
  name: String
  photo: Photo
  address: Address
  createdAt: AWSDateTime!
  updatedAt: AWSDateTime!
}

That seems to work?

Screenshot 2023-02-13 at 10 12 32

it maybe a little more complex than this actually.

You can specify a set when you want to just update a single value in the embedded object

So I think the schema would be something like

type Address {
  street: String
  city: String
  state: String
  zip: String
}

input AddressInput {
  street: String
  city: String
  state: String
  zip: String
}

input AddressInputWithSet {
  street: String
  city: String
  state: String
  zip: String
  set: AddressInput
}

input PhotoInput {
  height: Int
  width: Int
  url: String
}

input PhotoInputWithSet {
  height: Int
  width: Int
  url: String
  set: PhotoInput
}

Here is the prisma typescript for reference

  export type AddressNullableCreateEnvelopeInput = {
    set?: AddressCreateInput | null
  }

  export type AddressCreateInput = {
    street: string
    city: string
    zip: string
  }

I also need this feature since we heavily rely on MongoDB in production. Is anyone working on this atm? I can open a PR

maoosi commented

@giovanni-orciuolo I don't have experience with MongoDB, so PRs are more than welcomed on this one!