aws-amplify / amplify-codegen

Amplify Codegen is a JavaScript toolkit library for frontend and mobile developers building Amplify applications.
Apache License 2.0
59 stars 59 forks source link

Do we fully support fragments? #29

Open brunostuani opened 5 years ago

brunostuani commented 5 years ago

Which Category is your question related to? GraphQL transformer codegen

Provide additional details e.g. code snippets I'm adding a fragment in a custom GraphQL query file. For example:

fragment SheetParts on Sheet {
  id,
  name,
  bpm,
}

query ListItemPage($id: ID!) {
  getList(id: $id) {
    name,
    sheets {
      items {
        ...SheetParts
      }
    }
  }
}

query MyLibraryPage($limit: Int, $nextToken: String) {
  listSheets(limit: $limit, nextToken: $nextToken) {
    nextToken,
    items {
      ...SheetParts
    }
  }
}

Codegen gracefully generates SheetPartsFragment into my API, but when requesting the qery through the generated API, I get this error:

core.js:15724 ERROR Error: Uncaught (in promise): Object: {"data":null,"errors":[{"path":null,"locations":[{"line":9,"column":9,"sourceName":null}],"message":"Validation error of type UndefinedFragment: Undefined fragment SheetParts @ 'getList/sheets/items'"}]}

Am I missing something?

Thanks

brunostuani commented 5 years ago

This seems a bug in the angular service generation. The fragment definition is missing in the generated API queries. It works if I manually edit the generated service and add the missing fragment after the query.

SwaySway commented 4 years ago

@brunostuani Currently Amplify Codegen does not support fragments. Marking this as an enhancement.

ChristopheBougere commented 4 years ago

As my GraphQL schema is getting bigger, I was looking for a similar solution and just discovered fragments. This would be a very nice addition to the CLI!

Is there a way to make it work by bypassing the codegen part, or are fragments simply not usable on Amplify ? Here is the alternative I was considering (a client query builder): https://github.com/atulmy/gql-query-builder But having this handled server-side would be much better.

artemkloko commented 3 years ago

I have been using a hacky implementation for some time now and finally managed to assemble it as a plugin https://github.com/artemkloko/amplify-graphql-fragments-generator

It supports only TypeScript and is not for production. Still would be nice if someone could test it on a playground project or comment on the strategy itself.

jcbdev commented 3 years ago

@artemkloko I'm really liking the look of that library! I have also created your first issue! (sorry!) - https://github.com/artemkloko/amplify-graphql-fragments-generator/issues/1

let me know if you need any help with the issue 👍

armenr commented 9 months ago

I found a different way of doing this myself. I'm leaving it here for anyone that wants to use it as well. So far, it's been working for us, and it preserves the types we expect to see, with minimal effort and minimal verbosity.

It takes care of the the SERVER-SIDE problem of only asking for what you need, rather than having to ask for everything all the time ...which matters. A lot, in fact. 😎

We're doing this in a Nuxt3 SSR project, so there's a heck of a lot of bells and whistles.

Ingredient 1: A Function that consumes a codegen-generated Query

// Import the necessary modules from 'graphql' and '@aws-amplify/api-graphql'
import { parse, print } from "graphql";
import type { GraphQLResult } from "@aws-amplify/api-graphql";

// Complex/Contrived Usage Examples:

// import auto-generated query from Amplify
import { getRoom } from "~/graphql/queries";

// Define a type for generated queries. This type is a string (the query itself)
// and an object with __generatedQueryOutput and __generatedQueryInput properties.
// OutputType is the type of the data returned by the query, and InputType is the
// type of the variables passed to the query.
type CodeGenQuery<OutputType, InputType> = string & {
  __generatedQueryOutput: OutputType;
  __generatedQueryInput: InputType;
};

/**
 * Use a generic GraphQL query to dynamically generate lean queries with partial return fields and explicit sub-fields.
 *
 * This function takes a typed "full" GQL query and its typed query variables.
 * It dynamically generates a new GQL query to return only the requested fields, then
 * executes that query using the lean version of the query string. The lean query
 * string only includes and returns the fields specified in the `fields` parameter, putting
 * that concern and work rightfully on the server, rather than asking for everything and dealing with that
 * on the client. Most importantly, it utilizes the existing types generated in the GQL introspection
 * schema and requires no Type Assertions on the "lean query response" object. It also cuts down on DB usage
 * and load by ORDERS of magnitude, while still allowing for DEEP and extensible query depths (maxDepth) without
 * cost or performance impact.
 *
 * We preserve/maintain the original types on graphQL's auto-generated queries, saving you
 * many hours of pointless work, and many hundreds of lines of repetitive, useless query permutations.
 *
 * @param args - An object with the following properties:
 *   - query: The original, typed query auto-generated/CodeGen-ed by GQL.
 *   - variables: The typed variables to pass to the query.
 *   - fields: The fields or nested fields to include in the lean query string.
 *
 * @returns A promise that resolves to the correctly-typed result of the GraphQL query.
 */

export default async function useLeanQuery<T, V>(args: {
  query: CodeGenQuery<T, V>;
  variables: V;
  fields: string[];
}): Promise<GraphQLResult<T>> {
  // Parse the query string into a GraphQL Abstract Syntax Tree (AST)
  const document = parse(args.query);

  // Find the first field in the query
  const firstDefinition = document.definitions[0];

  // Check if the first definition has a selection set
  if ("selectionSet" in firstDefinition) {
    const firstField = firstDefinition.selectionSet.selections[0];

    // Check if the first field has a selection set
    if (firstField && "selectionSet" in firstField && firstField.selectionSet) {
      // Replace the selections of the first field with the specified fields
      firstField.selectionSet.selections = args.fields.map((field) => ({
        kind: "Field",
        name: { kind: "Name", value: field },
      }));
    }
  }

  // Convert the modified GraphQL AST back into a string
  const leanQuery = print(document);

  // Perform the GraphQL query using the lean query and the provided variables
  // useAPI() is a composable that returns an instance of the Amplify GraphQL API client class
  return (await useAPI().graphql({
    query: leanQuery,
    variables: args.variables,
  })) as GraphQLResult<T>;
}

// Contrived/Complex Usage Examples:

/**
 * A chat room (Room) model has many associated Users and Messages.
 * Message and User models associated to a Room model record also carry many more associations and nested Models of
 * their own.
 *
 * We just want the following fields and data from the chat room, without also having to return LOADS of User model and
 * Message model instances, each with their own sub-nested sets of data.
 *
 * When you use the auto-gen queries, you don't get to declare the return fields...so you get the whole jungle,
 * not just the banana.
 *
 * That's expensive on the client, expensive on the API, and expensive on Dynamo.
 * So here, we query on return fields, AND nested fields on those
 */

import { getRoom } from "~/graphql/queries";

async function getRoomData() {
  const roomAgain = await useLeanQuery({
    query: getRoom,
    variables: { id: "ef6338c3-7f7f-4b44-bc6c-c7feeba8dc45" },
    fields: [
      "name",
      "id",
      "users { items { id, user { firstName, lastName } } }",
      "messages { items { id, userId, content } }",
    ],
  });

  /**
   * Instances of the User model carry massive amounts of nested data via Associations (Messages, Rooms, Uploads, etc.).
   * When you use the auto-gen queries, you don't get to declare the return fields...so you get the whole jungle,
   * not just the banana.
   * 
   * ...we don't want all of them OR need ANY of them for this query, so why do we *have* to receive all of it?
   */

  import { getUser } from "~/graphql/queries";

  const getUserInfo = await useLeanQuery({
    query: getUser,
    variables: { id: "61cccc43-6b4d-4c46-bbc8-d15cc7ebeb9e" },
    fields: [
      "id",
      "emailAddress",
      "phoneNumber",
      "firstName",
      "lastName",
      "birthdate",
      "profilePicture",
      "createdAt",
      "updatedAt",
    ],
  });
}

... That's it!

Now use it and be amazed

Code

import { getRoom } from '@/graphql/queries'
// if you're not using Nuxt, just import the useLeanQuery function from wherever you put it

const getRoomName = await useLeanQuery({
  query: getRoom,
  variables: {
    id: 'ef6338c3-7f7f-4b44-bc6c-c7feeba8dc45',
  },
  fields: ['name', 'id'],
})

// Notice how there's no issue with IntelliSense or with Type inference!
console.log('getRoomName', getRoomName.data.getRoom)

Console output

getRoomName { name: 'TEST-ROOM', id: 'ef6338c3-7f7f-4b44-bc6c-c7feeba8dc45' }

What the "Room" Model actually looks like:

type Room @model @auth(rules: [{ allow: public }]) {
  id: ID!
  name: String
  users: [RoomUser] @hasMany(indexName: "byRoom", fields: ["id"])
  messages: [Message] @hasMany(indexName: "byRoom", fields: ["id"])
  group: Group @belongsTo(fields: ["groupId"])
  groupId: ID @index(name: "byGroup")
  project: Project @belongsTo(fields: ["projectId"])
  projectId: ID @index(name: "byProject")
}

🪄 MAGIC.

Leaving this here for anyone else who has been stuck like so many other people asking for this same thing, for so long.

Edit/Update

You can even do things like this (for nested fields on models via relationships):

async function getRoomData() {
  const roomAgain = await useLeanQuery({
    query: getRoom,
    variables: {
      id: 'ef6338c3-7f7f-4b44-bc6c-c7feeba8dc45',
    },
    fields: [
      'name',
      'id',
      'users { items { id, user { firstName, lastName } } }',
      'messages { items { id, userId, content } }',
    ],
  })
AnilMaktala commented 8 months ago

Hey @brunostuani 👋, Apologies for the significant delay in responding. The issue has been resolved in the latest Amplify CLI version. Please test it with the latest CLI and let us know if there are any other issues.