aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.41k stars 3.8k forks source link

@aws-cdk/aws-appsync-alpha: Generate TypeScript type definitions when using code-first schema approach #19668

Open adambiggs opened 2 years ago

adambiggs commented 2 years ago

Description

When using the code-first schema approach, it would be very helpful if CDK could generate TypeScript type definitions to match the generated GraphQL types, similar to how GraphQL Nexus does it.

Use Case

This would be especially useful with Lambda data sources, since currently types need to be manually defined in order to use AppSyncResolverHandler and/or AppSyncResolverEvent typings from the @types/aws-lambda package.

Proposed Solution

Similar to GraphQL Nexus, CDK could generate an e.g. appsync.d.ts file in the root of the project. And maybe the output path could be customized in cdk.json context.

Other information

No response

Acknowledge

giseburt commented 2 years ago

:+1: - We'd also like to see this.

A good first step would be a clean way to get the schema out of the API.

Here's what we do as a workaround (in TypeScript):

In your api-stack.ts or whatever:

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

// Added for export of schema
import { CfnGraphQLSchema } from "aws-cdk-lib/aws-appsync";
import * as fs from "fs";
import * as prettier from "prettier"; // optional use of prettier to clean it up

const API_NAME = "API";
export interface ApiStackProps extends cdk.StackProps {
  // ...
}

export class ApiStack extends cdk.Stack {

  api: GraphqlApi;

  constructor(scope: Construct, id: string, props: ApiStackProps) {
    super(scope, id, props);

    this.api = new GraphqlApi(this, API_NAME, {
      // ...
    });

    // ADD API schema here
  }

  exportSchema(schemaDir: string, schemaFile: string) {
    try {
      const definition = this.resolve((this.api.schema as unknown as { schema: CfnGraphQLSchema }).schema.definition);

      const header = `# This file is automatically generated during cdk synth
  # Do not edit this file, instead edit lib/gx-graphql-schema.ts, etc.

  `;

      fs.mkdir(schemaDir, { recursive: true }, (err) => {
        if (err) {
          throw Error(`Failed to make directory ${schemaDir}: ${err}`);
        }

        const schema = header + definition;

        // using prettier here, but the schema is in the schema variable now
        prettier.resolveConfig(schemaFile).then((options) => {
          if (!options) {
            throw Error("Huh?");
          }
          const formatted = prettier.format(schema, { ...options, filepath: schemaFile });
          fs.writeFile(schemaFile, formatted, (err) => {
            if (err) {
              throw Error(`Failed to write file ${schemaFile}: ${err}`);
            }
          });
        });
      });
    } catch (err) {
      console.error("Error reading out GraphQL Schema", err);
    }
  }
}

Then in your app:

  const apiStack = new ApiStack(app, API_STACK_NAME_BASE, {
    //...
  });
  // possibly add more to the schema here
  // then export it - adding to the schema after this point is an error
  apiStack.exportSchema(GENERATED_SCHEMA_DIR, GENERATED_SCHEMA_FILE);

You have to export it outside of the constructor, or you run into issues. This intentionally doesn't use async in exportSchema to not complicate calling it from the app with node 12 and 14.

For those who are interested, we then use the generated schema to further generate a "TypeScript Generic SDK" with GraphQL Code Generator, and further connect that to Apollo and some pre-made queries to make a ready-to-go client library.

GraphQL Code Generator "TypeScript Generic SDK" plugin notes here There's a little more work to then make use of that SDK with your GraphQL client (such as Apollo), as described at the bottom of that link.

kyptov commented 2 years ago

The main problem I see here that schema becomes source of truth which always conflicts with already persisted data. We can easily delete one value from GraphQL's enum, but deleted value can still returns from database.

So the source of truth must be enum in Typescript file which leads to having two enums and mapper like MyEnum <-> ApiMyEnum. It would be great to generate schema using existing typescripts's types and enums.

Something like type-graphql

SchollSimon commented 2 years ago

I agree with the thread creator this is a much needed feature for AppSync cdk. Working with code first approach is currently a very painful experience.

The main problem I see here that schema becomes source of truth which always conflicts with already persisted data. We can easily delete one value from GraphQL's enum, but deleted value can still returns from database.

This is a normal behaviour which occurs when changing your datamodel, you would need to do an migration to your data anyways.

In our project we are using the following workflow for generating type definitions from cdk in conjunction with a code first approach to compose our API with a large number of different API stacks.

After running cdk synth the schema is synthezised into a 'STACKNAME.template.json' in 'cdk.out' folder. We wrote a node script which does a lookup for the gnerated schema in the corresponding stackfile, which then generates a valid graphql.schema file in a folder called 'appsync' in the projects root folder.

const fs = require('fs');

const cdkOutDir = 'cdk.out';
fs.readdir(cdkOutDir, (e, files) => {
  if (e) throw e;

  for (let i = 0; i < files.length; i++) {
    const file = files[i];
    if (file.startsWith('YOURSTACK.template')) {
      fs.readFile(`${cdkOutDir}/${file}`, 'utf8', (err, data) => {
        if (err) throw err;

        const definition = Object.entries(JSON.parse(data).Resources).find(
          ([key, _value]) => key.startsWith('APINAMESchema')
        )[1].Properties.Definition;

        const outDir = 'appsync';
        if (!fs.existsSync(outDir)) {
          fs.mkdir(outDir, (error) => {
            if (error) throw error;
          });
        }
        fs.writeFile(`${outDir}/schema.graphql`, definition, (error) => {
          if (error) throw error;
        });
      });

      console.log(file);
      break;
    }
  }
});

After generating the graphql schema file, we installed AWS amplify cli and created a config file called '.graphqlconfig.yml' in the the newly generated 'appsync' folder :

projects:
  Codegen Project:
    schemaPath: schema.graphql
    includes:
      - ../src/graphql/**/*.ts
    excludes:
      - ./amplify/**
    extensions:
      amplify:
        codeGenTarget: typescript
        generatedFileName: ../src/graphql/types.ts
        docsFilePath: ../src/graphql
        region: us-east-1
        apiId: null
        frontend: javascript
        framework: none
        maxDepth: 4
extensions:
  amplify:
    version: 3

Add the following script entries to your package.json. We are using yarn but works with npm aka node as well:

"schema": "cdk synth && node scripts/generateSchema.js",
"codegen": "yarn schema && cd ./appsync && amplify codegen",`

If you now run yarn codegen -> you get the type definitions as well as queries, mutations and subscriptions generated into your project.

EYssel commented 1 year ago

In semi-relation to this issue. (Please let me know if not applicable in this thread)

Is anyone aware of a way to convert a typescript type to a graphql type. Maybe by using some typescript wizardry to transform an existing type?

This will sort of achieve the same objective, but without requiring codegen as the you write the type first, then generate a graph ql type from that. You can just use the type as needed.

  1. Create TS type
type User = {
   id: string,
   name: string,
}
  1. Convert TS type to GraphQL type
// end result

type UserGQL = {
  id: GraphqlType  // ID!
  name: GraphqlType // String!
}
  1. Use in Code First schema
schema.addType(UserGQL)
  1. Use the TS type in your code (Such as Lambda functions)
    const user: User = {
    id: '123',
    name: 'Bob'
    }

No codegen required.

I think this will be a great solution, but I might be a bit naive in regards to the TS limitations etc. (Still quite new with TS and CDK)