diff --git a/.codebuild/e2e_workflow.yml b/.codebuild/e2e_workflow.yml index 9019bd5a2..e75a50bb1 100644 --- a/.codebuild/e2e_workflow.yml +++ b/.codebuild/e2e_workflow.yml @@ -121,22 +121,23 @@ batch: depend-on: - publish_to_local_registry - identifier: >- - build_app_ts_uninitialized_project_modelgen_android_uninitialized_project_modelgen_flutter_uninitialized_project_modelgen_ios + build_app_ts_uninitialized_project_codegen_js_uninitialized_project_modelgen_android_uninitialized_project_modelgen_flutter buildspec: .codebuild/run_e2e_tests.yml env: compute-type: BUILD_GENERAL1_LARGE variables: TEST_SUITE: >- - src/__tests__/build-app-ts.test.ts|src/__tests__/uninitialized-project-modelgen-android.test.ts|src/__tests__/uninitialized-project-modelgen-flutter.test.ts|src/__tests__/uninitialized-project-modelgen-ios.test.ts + src/__tests__/build-app-ts.test.ts|src/__tests__/uninitialized-project-codegen-js.test.ts|src/__tests__/uninitialized-project-modelgen-android.test.ts|src/__tests__/uninitialized-project-modelgen-flutter.test.ts CLI_REGION: ap-southeast-1 depend-on: - publish_to_local_registry - - identifier: uninitialized_project_modelgen_js + - identifier: uninitialized_project_modelgen_ios_uninitialized_project_modelgen_js buildspec: .codebuild/run_e2e_tests.yml env: compute-type: BUILD_GENERAL1_LARGE variables: - TEST_SUITE: src/__tests__/uninitialized-project-modelgen-js.test.ts + TEST_SUITE: >- + src/__tests__/uninitialized-project-modelgen-ios.test.ts|src/__tests__/uninitialized-project-modelgen-js.test.ts CLI_REGION: ap-southeast-2 depend-on: - publish_to_local_registry diff --git a/packages/amplify-codegen-e2e-core/src/categories/codegen.ts b/packages/amplify-codegen-e2e-core/src/categories/codegen.ts index 0740e90f3..ab5a83a1f 100644 --- a/packages/amplify-codegen-e2e-core/src/categories/codegen.ts +++ b/packages/amplify-codegen-e2e-core/src/categories/codegen.ts @@ -30,9 +30,40 @@ export const generateModelsWithOptions = (cwd: string, options: Record { +export function generateStatementsAndTypes(cwd: string, errorMessage?: string) : Promise { return new Promise((resolve, reject) => { - spawn(getCLIPath(), ['codegen'], { cwd, stripColors: true }) + const chain = spawn(getCLIPath(), ['codegen'], { cwd, stripColors: true }) + + if (errorMessage) { + chain.wait(errorMessage); + } + + return chain.run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }) + }); +} + +export function generateStatements(cwd: string) : Promise { + return new Promise((resolve, reject) => { + spawn(getCLIPath(), ['codegen', 'statements'], { cwd, stripColors: true }) + .run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }) + }); +} + +export function generateTypes(cwd: string) : Promise { + return new Promise((resolve, reject) => { + spawn(getCLIPath(), ['codegen', 'types'], { cwd, stripColors: true }) .run((err: Error) => { if (!err) { resolve(); @@ -46,7 +77,10 @@ export function generateStatementsAndTypes(cwd: string) : Promise { // CLI workflow to add codegen to Amplify project export function addCodegen(cwd: string, settings: any = {}): Promise { return new Promise((resolve, reject) => { - const chain = spawn(getCLIPath(), ['codegen', 'add'], { cwd, stripColors: true }); + const params = settings.params + ? ['codegen', 'add', ...settings.params] + : ['codegen', 'add']; + const chain = spawn(getCLIPath(), params, { cwd, stripColors: true }); if (settings.isAPINotAdded) { chain.wait("There are no GraphQL APIs available."); chain.wait("Add by running $amplify api add"); @@ -166,22 +200,26 @@ export function generateModelIntrospection(cwd: string, settings: { outputDir?: } // CLI workflow to add codegen to non-Amplify JS project -export function addCodegenNonAmplifyJS(cwd: string): Promise { +export function addCodegenNonAmplifyJS(cwd: string, params: Array, initialFailureMessage?: string): Promise { return new Promise((resolve, reject) => { - const cmdOptions = ['codegen', 'add', '--apiId', 'mockapiid']; - const chain = spawn(getCLIPath(), cmdOptions, { cwd, stripColors: true }); - chain - .wait("Choose the type of app that you're building") - .sendCarriageReturn() - .wait('What javascript framework are you using') - .sendCarriageReturn() - .wait('Choose the code generation language target').sendCarriageReturn() - .wait('Enter the file name pattern of graphql queries, mutations and subscriptions') - .sendCarriageReturn() - .wait('Do you want to generate/update all possible GraphQL operations') - .sendLine('y') - .wait('Enter maximum statement depth [increase from default if your schema is deeply') - .sendCarriageReturn(); + const chain = spawn(getCLIPath(), ['codegen', 'add', ...params], { cwd, stripColors: true }); + + if (initialFailureMessage) { + chain.wait(initialFailureMessage) + } else { + chain + .wait("Choose the type of app that you're building") + .sendCarriageReturn() + .wait('What javascript framework are you using') + .sendCarriageReturn() + .wait('Choose the code generation language target').sendCarriageReturn() + .wait('Enter the file name pattern of graphql queries, mutations and subscriptions') + .sendCarriageReturn() + .wait('Do you want to generate/update all possible GraphQL operations') + .sendLine('y') + .wait('Enter maximum statement depth [increase from default if your schema is deeply') + .sendCarriageReturn(); + } chain.run((err: Error) => { if (!err) { @@ -192,3 +230,31 @@ export function addCodegenNonAmplifyJS(cwd: string): Promise { }); }); } + +export function addCodegenNonAmplifyTS(cwd: string, params: Array, initialFailureMessage?: string): Promise { + return new Promise((resolve, reject) => { + const chain = spawn(getCLIPath(), ['codegen', 'add', ...params], { cwd, stripColors: true }); + + if (initialFailureMessage) { + chain.wait(initialFailureMessage) + } else { + chain + .wait("Choose the type of app that you're building").sendCarriageReturn() + .wait('What javascript framework are you using').sendCarriageReturn() + .wait('Choose the code generation language target').sendKeyDown().sendCarriageReturn() + .wait('Enter the file name pattern of graphql queries, mutations and subscriptions').sendCarriageReturn() + .wait('Do you want to generate/update all possible GraphQL operations').sendLine('y') + .wait('Enter maximum statement depth [increase from default if your schema is deeply').sendCarriageReturn() + .wait('Enter the file name for the generated code').sendCarriageReturn() + .wait('Do you want to generate code for your newly created GraphQL API').sendCarriageReturn(); + } + + chain.run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} \ No newline at end of file diff --git a/packages/amplify-codegen-e2e-tests/schemas/sdl/schema.graphql b/packages/amplify-codegen-e2e-tests/schemas/sdl/schema.graphql new file mode 100644 index 000000000..19f1eb1b3 --- /dev/null +++ b/packages/amplify-codegen-e2e-tests/schemas/sdl/schema.graphql @@ -0,0 +1,11 @@ +type Query { + echo: String +} + +type Mutation { + mymutation: String +} + +type Subscription { + mysub: String +} diff --git a/packages/amplify-codegen-e2e-tests/schemas/sdl/schema.json b/packages/amplify-codegen-e2e-tests/schemas/sdl/schema.json new file mode 100644 index 000000000..b133d772b --- /dev/null +++ b/packages/amplify-codegen-e2e-tests/schemas/sdl/schema.json @@ -0,0 +1,1163 @@ +{ + "data": { + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": { + "name": "Mutation" + }, + "subscriptionType": { + "name": "Subscription" + }, + "types": [ + { + "kind": "OBJECT", + "name": "Query", + "description": null, + "fields": [ + { + "name": "echo", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "Built-in String", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Mutation", + "description": null, + "fields": [ + { + "name": "mymutation", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Subscription", + "description": null, + "fields": [ + { + "name": "mysub", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Introspection defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "'A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "'If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": null, + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given __Type is", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "Built-in Boolean", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onOperation", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onFragment", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onField", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "An enum describing valid locations where a directive can be placed", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Indicates the directive is valid on queries.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Indicates the directive is valid on mutations.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Indicates the directive is valid on fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Indicates the directive is valid on fragment definitions.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Indicates the directive is valid on fragment spreads.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Indicates the directive is valid on inline fragments.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Indicates the directive is valid on a schema SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Indicates the directive is valid on a scalar SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates the directive is valid on an object SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Indicates the directive is valid on a field SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Indicates the directive is valid on a field argument SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates the directive is valid on an interface SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates the directive is valid on an union SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates the directive is valid on an enum SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Indicates the directive is valid on an enum value SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates the directive is valid on an input object SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Indicates the directive is valid on an input object field SDL definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": true, + "onField": true + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if`'argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": true, + "onField": true + }, + { + "name": "defer", + "description": "This directive allows results to be deferred during execution", + "locations": [ + "FIELD" + ], + "args": [], + "onOperation": false, + "onFragment": false, + "onField": true + }, + { + "name": "aws_api_key", + "description": "Tells the service this field/object has access authorized by an API key.", + "locations": [ + "OBJECT", + "FIELD_DEFINITION" + ], + "args": [], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_iam", + "description": "Tells the service this field/object has access authorized by sigv4 signing.", + "locations": [ + "OBJECT", + "FIELD_DEFINITION" + ], + "args": [], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_oidc", + "description": "Tells the service this field/object has access authorized by an OIDC token.", + "locations": [ + "OBJECT", + "FIELD_DEFINITION" + ], + "args": [], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_lambda", + "description": "Tells the service this field/object has access authorized by a Lambda Authorizer.", + "locations": [ + "OBJECT", + "FIELD_DEFINITION" + ], + "args": [], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_subscribe", + "description": "Tells the service which mutation triggers this subscription.", + "locations": [ + "FIELD_DEFINITION" + ], + "args": [ + { + "name": "mutations", + "description": "List of mutations which will trigger this subscription when they are called.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_cognito_user_pools", + "description": "Tells the service this field/object has access authorized by a Cognito User Pools token.", + "locations": [ + "OBJECT", + "FIELD_DEFINITION" + ], + "args": [ + { + "name": "cognito_groups", + "description": "List of cognito user pool groups which have access on this field", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "deprecated", + "description": null, + "locations": [ + "FIELD_DEFINITION", + "ENUM_VALUE" + ], + "args": [ + { + "name": "reason", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"" + } + ], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_auth", + "description": "Directs the schema to enforce authorization on a field", + "locations": [ + "FIELD_DEFINITION" + ], + "args": [ + { + "name": "cognito_groups", + "description": "List of cognito user pool groups which have access on this field", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": false, + "onField": false + }, + { + "name": "aws_publish", + "description": "Tells the service which subscriptions will be published to when this mutation is called. This directive is deprecated use @aws_susbscribe directive instead.", + "locations": [ + "FIELD_DEFINITION" + ], + "args": [ + { + "name": "subscriptions", + "description": "List of subscriptions which will be published to when this mutation is called.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": false, + "onField": false + } + ] + } + } +} \ No newline at end of file diff --git a/packages/amplify-codegen-e2e-tests/src/__tests__/add-codegen-js.test.ts b/packages/amplify-codegen-e2e-tests/src/__tests__/add-codegen-js.test.ts index a78fbc75a..2a23c972d 100644 --- a/packages/amplify-codegen-e2e-tests/src/__tests__/add-codegen-js.test.ts +++ b/packages/amplify-codegen-e2e-tests/src/__tests__/add-codegen-js.test.ts @@ -6,13 +6,17 @@ import { createRandomName, addApiWithoutSchema, updateApiSchema, - addCodegenNonAmplifyJS } from "@aws-amplify/amplify-codegen-e2e-core"; -import { existsSync, writeFileSync } from "fs"; +import { existsSync } from "fs"; import path from 'path'; import { isNotEmptyDir } from '../utils'; -import { deleteAmplifyProject, testAddCodegen, testSetupBeforeAddCodegen, -getGraphQLConfigFilePath, testValidGraphQLConfig } from '../codegen-tests-base'; +import { + deleteAmplifyProject, + testAddCodegen, + testSetupBeforeAddCodegen, + getGraphQLConfigFilePath, + testValidGraphQLConfig, +} from '../codegen-tests-base'; const schema = 'simple_model.graphql'; @@ -74,35 +78,7 @@ describe('codegen add tests - JS', () => { await testAddCodegen(config, projectRoot, schema); }); - it(`Adding codegen outside of Amplify project`, async () => { - // init project and add API category - const testSchema = ` - type Query { - echo: String - } - - type Mutation { - mymutation: String - } - - type Subscription { - mysub: String - } - `; - - // Setup the non-amplify project with schema and pre-existing files - const userSourceCodePath = testSetupBeforeAddCodegen(projectRoot, config); - const schemaPath = path.join(projectRoot, 'schema.graphql'); - writeFileSync(schemaPath, testSchema); - - // add codegen without init - await expect(addCodegenNonAmplifyJS(projectRoot)).resolves.not.toThrow(); - - // pre-existing file should still exist - expect(existsSync(userSourceCodePath)).toBe(true); - // GraphQL statements are generated - expect(isNotEmptyDir(path.join(projectRoot, config.graphqlCodegenDir))).toBe(true); - // graphql configuration should be added - expect(existsSync(getGraphQLConfigFilePath(projectRoot))).toBe(true); + it(`supports add codegen with redundant region parameter`, async () => { + await testAddCodegen(config, projectRoot, schema, ['--region', 'us-fake-1']); }); -}); \ No newline at end of file +}); diff --git a/packages/amplify-codegen-e2e-tests/src/__tests__/uninitialized-project-codegen-js.test.ts b/packages/amplify-codegen-e2e-tests/src/__tests__/uninitialized-project-codegen-js.test.ts new file mode 100644 index 000000000..076e7b952 --- /dev/null +++ b/packages/amplify-codegen-e2e-tests/src/__tests__/uninitialized-project-codegen-js.test.ts @@ -0,0 +1,114 @@ +import { createNewProjectDir, DEFAULT_JS_CONFIG, AmplifyFrontend, generateStatementsAndTypes } from '@aws-amplify/amplify-codegen-e2e-core'; +import path from 'path'; +import { deleteAmplifyProject, testAddCodegenUninitialized } from '../codegen-tests-base'; +import { rmSync } from "fs-extra"; + +describe('codegen add tests - JS', () => { + let projectRoot: string; + const javascriptConfig = DEFAULT_JS_CONFIG; + const typescriptConfig = { + ...DEFAULT_JS_CONFIG, + frontendType: AmplifyFrontend.typescript, + }; + + beforeEach(async () => { + projectRoot = await createNewProjectDir('uninitializedProjectCodegenJS'); + }); + + afterEach(async () => { + await deleteAmplifyProject(projectRoot); + }); + + it(`graphql sdl file`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: javascriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.js', 'queries.js', 'subscriptions.js'], + }); + }); + + it(`region is ignored if schema file is provided`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: javascriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.js', 'queries.js', 'subscriptions.js'], + additionalParams: ['--region', 'us-fake-1'], + }); + }); + + it(`json sdl file`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: javascriptConfig, + sdlFilename: 'schema.json', + expectedFilenames: ['mutations.js', 'queries.js', 'subscriptions.js'], + }); + }); + + it(`typescript`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: typescriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.ts', 'queries.ts', 'subscriptions.ts'], + }); + }); + + it(`drop and regenerate`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: typescriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.ts', 'queries.ts', 'subscriptions.ts'], + dropAndRunCodegen: true, + }); + }); + + it(`drop and regenerate statements`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: typescriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.ts', 'queries.ts', 'subscriptions.ts'], + dropAndRunCodegenStatements: true, + }); + }); + + it(`drop and regenerate types`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: typescriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.ts', 'queries.ts', 'subscriptions.ts'], + dropAndRunCodegenStatements: true, + dropAndRunCodegenTypes: true, + }); + }); + + it(`throws a sane warning on missing graphqlconfig file`, async () => { + // Add codegen + await testAddCodegenUninitialized({ + projectRoot, + config: javascriptConfig, + sdlFilename: 'schema.graphql', + expectedFilenames: ['mutations.js', 'queries.js', 'subscriptions.js'], + }); + + // Remove .graphqlconfig.yml file + rmSync(path.join(projectRoot, '.graphqlconfig.yml')); + + // Run and expect failure message + await generateStatementsAndTypes(projectRoot, 'code generation is not configured'); + }); + + it(`throws a sane warning on missing sdl schema and no api id specified`, async () => { + await testAddCodegenUninitialized({ + projectRoot, + config: javascriptConfig, + expectedFilenames: ['mutations.js', 'queries.js', 'subscriptions.js'], + initialFailureMessage: 'Provide an AppSync API ID with --apiId or manually download schema.graphql or schema.json' + }); + }); +}); diff --git a/packages/amplify-codegen-e2e-tests/src/codegen-tests-base/add-codegen.ts b/packages/amplify-codegen-e2e-tests/src/codegen-tests-base/add-codegen.ts index 9df7e9d96..4ae362489 100644 --- a/packages/amplify-codegen-e2e-tests/src/codegen-tests-base/add-codegen.ts +++ b/packages/amplify-codegen-e2e-tests/src/codegen-tests-base/add-codegen.ts @@ -4,14 +4,20 @@ import { updateApiSchema, addCodegen, AmplifyFrontendConfig, - createRandomName + createRandomName, + addCodegenNonAmplifyJS, + addCodegenNonAmplifyTS, + AmplifyFrontend, + generateStatementsAndTypes, + generateStatements, + generateTypes, } from "@aws-amplify/amplify-codegen-e2e-core"; -import { existsSync } from "fs"; +import { existsSync, readFileSync, writeFileSync, readdirSync, rmSync } from "fs"; import path from 'path'; import { isNotEmptyDir } from '../utils'; -import { testSetupBeforeAddCodegen, testValidGraphQLConfig } from "./test-setup"; +import { getGraphQLConfigFilePath, testSetupBeforeAddCodegen, testValidGraphQLConfig } from "./test-setup"; -export async function testAddCodegen(config: AmplifyFrontendConfig, projectRoot: string, schema: string) { +export async function testAddCodegen(config: AmplifyFrontendConfig, projectRoot: string, schema: string, additionalParams?: Array) { // init project and add API category await initProjectWithProfile(projectRoot, { ...config }); const projectName = createRandomName(); @@ -21,7 +27,7 @@ export async function testAddCodegen(config: AmplifyFrontendConfig, projectRoot: const userSourceCodePath = testSetupBeforeAddCodegen(projectRoot, config); // add codegen succeeds - await expect(addCodegen(projectRoot, { ...config })).resolves.not.toThrow(); + await expect(addCodegen(projectRoot, { ...config, params: additionalParams ?? [] })).resolves.not.toThrow(); // pre-existing file should still exist expect(existsSync(userSourceCodePath)).toBe(true); @@ -29,3 +35,112 @@ export async function testAddCodegen(config: AmplifyFrontendConfig, projectRoot: expect(isNotEmptyDir(path.join(projectRoot, config.graphqlCodegenDir))).toBe(true); testValidGraphQLConfig(projectRoot, config); } + +export type TestAddCodegenUninitializedProps = { + config: AmplifyFrontendConfig; + projectRoot: string; + sdlFilename?: string; + expectedFilenames: Array; + dropAndRunCodegen?: boolean; + dropAndRunCodegenStatements?: boolean; + dropAndRunCodegenTypes?: boolean; + initialFailureMessage?: string; + additionalParams?: Array; +}; + +const assertTypeFileExists = (projectRoot: string): void => { + expect(existsSync(path.join(projectRoot, 'src', 'API.ts'))).toBe(true) +}; + +/** + * Ensure that all values provided in the expected set are present in the received set, allowing for additional values in received. + * @param expectedValues the expected values to check + * @param receivedValues the received values to check + */ +const ensureAllExpectedValuesAreReceived = (expectedValues: Array, receivedValues: Array): void => { + const receivedValueSet = new Set(receivedValues); + console.log(`Comparing received values: ${JSON.stringify(receivedValues)} to expected values: ${JSON.stringify(expectedValues)}`); + expectedValues.forEach((expectedFilename) => expect(receivedValueSet.has(expectedFilename)).toBe(true)); +}; + +export async function testAddCodegenUninitialized({ + config, + projectRoot, + sdlFilename, + expectedFilenames, + dropAndRunCodegen, + dropAndRunCodegenStatements, + dropAndRunCodegenTypes, + initialFailureMessage, + additionalParams, +}: TestAddCodegenUninitializedProps) { + // Setup the non-amplify project with schema and pre-existing files + const userSourceCodePath = testSetupBeforeAddCodegen(projectRoot, config); + + // Write SDL Schema + if (sdlFilename) { + const sdlSchema = readFileSync(path.join(__dirname, '..', '..', 'schemas', 'sdl', sdlFilename), 'utf-8'); + writeFileSync(path.join(projectRoot, sdlFilename), sdlSchema); + } + + // add codegen without init + switch (config.frontendType) { + case AmplifyFrontend.javascript: + await addCodegenNonAmplifyJS(projectRoot, additionalParams ?? [], initialFailureMessage); + break; + case AmplifyFrontend.typescript: + await addCodegenNonAmplifyTS(projectRoot, additionalParams ?? [], initialFailureMessage); + break; + default: + throw new Error(`Received unexpected frontendType ${config.frontendType}`); + } + + // return if we expected the add command to fail + if (initialFailureMessage) { + return; + } + + // pre-existing file should still exist + expect(existsSync(userSourceCodePath)).toBe(true); + // GraphQL statements are generated + ensureAllExpectedValuesAreReceived(expectedFilenames, readdirSync(path.join(projectRoot, config.graphqlCodegenDir))) + // graphql configuration should be added + expect(existsSync(getGraphQLConfigFilePath(projectRoot))).toBe(true); + if (config.frontendType === AmplifyFrontend.typescript) { + assertTypeFileExists(projectRoot) + } + + if (dropAndRunCodegen || dropAndRunCodegenStatements || dropAndRunCodegenTypes) { + rmSync(path.join(projectRoot, config.graphqlCodegenDir), { recursive: true }); + // pre-existing file should still exist + expect(existsSync(userSourceCodePath)).toBe(true); + // Graphql statements are deleted + expect(existsSync(path.join(projectRoot, config.graphqlCodegenDir))).toBe(false); + } + + if (dropAndRunCodegen) { + await generateStatementsAndTypes(projectRoot); + + // GraphQL statements are regenerated + ensureAllExpectedValuesAreReceived(expectedFilenames, readdirSync(path.join(projectRoot, config.graphqlCodegenDir))) + + if (config.frontendType === AmplifyFrontend.typescript) { + assertTypeFileExists(projectRoot) + } + } + + if (dropAndRunCodegenStatements) { + await generateStatements(projectRoot); + + // GraphQL statements are regenerated + ensureAllExpectedValuesAreReceived(expectedFilenames, readdirSync(path.join(projectRoot, config.graphqlCodegenDir))) + } + + if (dropAndRunCodegenTypes) { + await generateTypes(projectRoot); + + if (config.frontendType === AmplifyFrontend.typescript) { + assertTypeFileExists(projectRoot) + } + } +} diff --git a/packages/amplify-codegen/commands/codegen/add.js b/packages/amplify-codegen/commands/codegen/add.js index 89ac18581..0a64e45fd 100644 --- a/packages/amplify-codegen/commands/codegen/add.js +++ b/packages/amplify-codegen/commands/codegen/add.js @@ -9,14 +9,16 @@ module.exports = { try { const { options = {} } = context.parameters; const keys = Object.keys(options); - if (keys.length && !keys.includes('apiId')) { - const paramMsg = keys.length > 1 ? 'Invalid parameters ' : 'Invalid parameter '; + // frontend and framework are undocumented, but are read when apiId is also supplied + const { apiId = null, region, yes, frontend, framework, debug, ...rest } = options; + const extraOptions = Object.keys(rest); + if (extraOptions.length) { + const paramMsg = extraOptions.length > 1 ? 'Invalid parameters' : 'Invalid parameter'; context.print.info(`${paramMsg} ${keys.join(', ')}`); context.print.info(constants.INFO_MESSAGE_ADD_ERROR); return; } - const apiId = context.parameters.options.apiId || null; - await codeGen.add(context, apiId); + await codeGen.add(context, apiId, region); } catch (ex) { context.print.error(ex.message); } diff --git a/packages/amplify-codegen/package.json b/packages/amplify-codegen/package.json index ee71e86bb..296e27e59 100644 --- a/packages/amplify-codegen/package.json +++ b/packages/amplify-codegen/package.json @@ -46,7 +46,7 @@ "coverageThreshold": { "global": { "branches": 54, - "functions": 65, + "functions": 64, "lines": 72 } }, diff --git a/packages/amplify-codegen/src/commands/add.js b/packages/amplify-codegen/src/commands/add.js index 713e3194b..6b3a609a9 100644 --- a/packages/amplify-codegen/src/commands/add.js +++ b/packages/amplify-codegen/src/commands/add.js @@ -1,4 +1,5 @@ const Ora = require('ora'); +const process = require('process'); const { loadConfig } = require('../codegen-config'); const constants = require('../constants'); const generateStatements = require('./statements'); @@ -23,11 +24,11 @@ const askForFramework = require('../walkthrough/questions/selectFramework'); const frontends = ['android', 'ios', 'javascript']; const frameworks = ['angular', 'ember', 'ionic', 'react', 'react-native', 'vue', 'none']; -async function add(context, apiId = null) { +async function add(context, apiId = null, region = 'us-east-1') { let withoutInit = false; // Determine if working in an amplify project try { - context.amplify.getProjectMeta(); + await context.amplify.getProjectMeta(); } catch (e) { withoutInit = true; const config = loadConfig(context, withoutInit); @@ -37,9 +38,9 @@ async function add(context, apiId = null) { } const schemaPath = ['schema.graphql', 'schema.json'].map(p => path.join(process.cwd(), p)).find(p => fs.existsSync(p)); - if (withoutInit && !schemaPath) { + if (withoutInit && !(apiId || schemaPath)) { throw Error( - `Please download schema.graphql or schema.json and place in ${process.cwd()} before adding codegen when not in an amplify project`, + `Provide an AppSync API ID with --apiId or manually download schema.graphql or schema.json and place in ${process.cwd()} before adding codegen when not in an amplify project`, ); } // Grab the frontend @@ -69,7 +70,6 @@ async function add(context, apiId = null) { } } - let region = 'us-east-1'; if (!withoutInit) { region = getProjectAwsRegion(context); } @@ -78,36 +78,37 @@ async function add(context, apiId = null) { throw new Error(constants.ERROR_CODEGEN_SUPPORT_MAX_ONE_API); } let apiDetails; - if (!withoutInit) { - if (!apiId) { - const availableAppSyncApis = getAppSyncAPIDetails(context); // published and un-published - if (availableAppSyncApis.length === 0) { - throw new NoAppSyncAPIAvailableError(constants.ERROR_CODEGEN_NO_API_AVAILABLE); - } - [apiDetails] = availableAppSyncApis; - apiDetails.isLocal = true; - } else { - let shouldRetry = true; - while (shouldRetry) { - const apiDetailSpinner = new Ora(); - try { - apiDetailSpinner.start('Getting API details'); - apiDetails = await getAppSyncAPIInfo(context, apiId, region); - apiDetailSpinner.succeed(); + if (!withoutInit && !apiId) { + const availableAppSyncApis = getAppSyncAPIDetails(context); // published and un-published + if (availableAppSyncApis.length === 0) { + throw new NoAppSyncAPIAvailableError(constants.ERROR_CODEGEN_NO_API_AVAILABLE); + } + [apiDetails] = availableAppSyncApis; + apiDetails.isLocal = true; + } else if (apiId) { + let shouldRetry = true; + while (shouldRetry) { + const apiDetailSpinner = new Ora(); + try { + apiDetailSpinner.start('Getting API details'); + apiDetails = await getAppSyncAPIInfo(context, apiId, region); + if (!withoutInit) { await updateAmplifyMeta(context, apiDetails); - break; - } catch (e) { - apiDetailSpinner.fail(); - if (e instanceof AmplifyCodeGenAPINotFoundError) { - context.print.info(`AppSync API was not found in region ${region}`); - ({ shouldRetry, region } = await changeAppSyncRegion(context, region)); - } else { - throw e; - } + } + apiDetailSpinner.succeed(); + break; + } catch (e) { + apiDetailSpinner.fail(); + if (e instanceof AmplifyCodeGenAPINotFoundError) { + context.print.info(`AppSync API was not found in region ${region}`); + ({ shouldRetry, region } = await changeAppSyncRegion(context, region)); + } else { + throw e; } } } } + // else no appsync API, but has schema.graphql or schema.json if (!withoutInit && !apiDetails) { return; @@ -123,6 +124,8 @@ async function add(context, apiId = null) { } else { schema = getSDLSchemaLocation(apiDetails.name); } + } else if (apiDetails) { + schema = await downloadIntrospectionSchemaWithProgress(context, apiDetails.id, path.join(process.cwd(), 'schema.json'), region); } else { schema = schemaPath; } diff --git a/packages/amplify-codegen/src/commands/generateStatementsAndType.js b/packages/amplify-codegen/src/commands/generateStatementsAndType.js index 92459dbdc..f04b9c240 100644 --- a/packages/amplify-codegen/src/commands/generateStatementsAndType.js +++ b/packages/amplify-codegen/src/commands/generateStatementsAndType.js @@ -4,7 +4,7 @@ const generateTypes = require('./types'); const generateStatements = require('./statements'); const { loadConfig } = require('../codegen-config'); const constants = require('../constants'); -const { ensureIntrospectionSchema, getAppSyncAPIDetails } = require('../utils'); +const { ensureIntrospectionSchema, getAppSyncAPIDetails, getAppSyncAPIInfoFromProject } = require('../utils'); const path = require('path'); const fs = require('fs-extra'); @@ -17,30 +17,31 @@ async function generateStatementsAndTypes(context, forceDownloadSchema, maxDepth withoutInit = true; } - // Check if introspection schema exists - const schemaPath = ['schema.graphql', 'schema.json'].map(p => path.join(process.cwd(), p)).find(p => fs.existsSync(p)); - if (withoutInit && !schemaPath) { - throw Error( - `Please download the schema.graphql or schema.json and place in ${process.cwd()} before adding codegen when not in an amplify project` - ); - } - - if (withoutInit) { - forceDownloadSchema = false; - } const config = loadConfig(context, withoutInit); const projects = config.getProjects(); if (!projects.length) { throw new NoAppSyncAPIAvailableError(constants.ERROR_CODEGEN_NO_API_CONFIGURED); } + const project = projects[0]; + const schemaPath = ['schema.graphql', 'schema.json'].map(p => path.join(process.cwd(), p)).find(p => fs.existsSync(p)); + if (withoutInit && !project && !schemaPath) { + throw Error( + `Please download the schema.graphql or schema.json and place in ${process.cwd()} before adding codegen when not in an amplify project`, + ); + } + let apis = []; if (!withoutInit) { apis = getAppSyncAPIDetails(context); + } else { + const api = await getAppSyncAPIInfoFromProject(context, project); + if (api) { + apis = [api]; + } } if (!apis.length && !withoutInit) { throw new NoAppSyncAPIAvailableError(constants.ERROR_CODEGEN_NO_API_META); } - const project = projects[0]; const { frontend } = project.amplifyExtension; let projectPath = process.cwd(); if (!withoutInit) { @@ -48,10 +49,10 @@ async function generateStatementsAndTypes(context, forceDownloadSchema, maxDepth } let downloadPromises; - if (!withoutInit) { + if (apis.length) { downloadPromises = projects.map( async cfg => - await ensureIntrospectionSchema(context, join(projectPath, cfg.schema), apis[0], cfg.amplifyExtension.region, forceDownloadSchema) + await ensureIntrospectionSchema(context, join(projectPath, cfg.schema), apis[0], cfg.amplifyExtension.region, forceDownloadSchema), ); await Promise.all(downloadPromises); } diff --git a/packages/amplify-codegen/src/commands/statements.js b/packages/amplify-codegen/src/commands/statements.js index f87c1be26..86184b91d 100644 --- a/packages/amplify-codegen/src/commands/statements.js +++ b/packages/amplify-codegen/src/commands/statements.js @@ -4,7 +4,13 @@ const Ora = require('ora'); const { loadConfig } = require('../codegen-config'); const constants = require('../constants'); -const { ensureIntrospectionSchema, getFrontEndHandler, getAppSyncAPIDetails, readSchemaFromFile } = require('../utils'); +const { + ensureIntrospectionSchema, + getFrontEndHandler, + getAppSyncAPIDetails, + readSchemaFromFile, + getAppSyncAPIInfoFromProject, +} = require('../utils'); const { generateGraphQLDocuments } = require('@aws-amplify/graphql-docs-generator'); const { generateStatements: generateStatementsHelper } = require('@aws-amplify/graphql-generator'); @@ -16,9 +22,18 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou } const config = loadConfig(context, withoutInit); const projects = config.getProjects(); + if (!projects.length && withoutInit) { + context.print.info(constants.ERROR_CODEGEN_NO_API_CONFIGURED); + return; + } let apis = []; if (!withoutInit) { apis = getAppSyncAPIDetails(context); + } else { + const api = await getAppSyncAPIInfoFromProject(context, projects[0]); + if (api) { + apis = [api]; + } } let projectPath = process.cwd(); if (!withoutInit) { @@ -30,10 +45,6 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou return; } } - if (!projects.length && withoutInit) { - context.print.info(constants.ERROR_CODEGEN_NO_API_CONFIGURED); - return; - } for (const cfg of projects) { const includeFiles = path.join(projectPath, cfg.includes[0]); @@ -41,15 +52,11 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou ? path.join(projectPath, cfg.amplifyExtension.docsFilePath) : path.dirname(path.dirname(includeFiles)); const schemaPath = path.join(projectPath, cfg.schema); - let region; - let frontend; - if (!withoutInit) { - ({ region } = cfg.amplifyExtension); + if (apis.length) { + const { region } = cfg.amplifyExtension; await ensureIntrospectionSchema(context, schemaPath, apis[0], region, forceDownloadSchema); - frontend = getFrontEndHandler(context); - } else { - frontend = decoupleFrontend; } + const frontend = withoutInit ? cfg.amplifyExtension.frontend : getFrontEndHandler(context); const language = frontend === 'javascript' ? cfg.amplifyExtension.codeGenTarget : 'graphql'; const opsGenSpinner = new Ora(constants.INFO_MESSAGE_OPS_GEN); diff --git a/packages/amplify-codegen/src/commands/types.js b/packages/amplify-codegen/src/commands/types.js index c45c3b17c..5da70cddf 100644 --- a/packages/amplify-codegen/src/commands/types.js +++ b/packages/amplify-codegen/src/commands/types.js @@ -5,7 +5,7 @@ const glob = require('glob-all'); const constants = require('../constants'); const { loadConfig } = require('../codegen-config'); -const { ensureIntrospectionSchema, getFrontEndHandler, getAppSyncAPIDetails } = require('../utils'); +const { ensureIntrospectionSchema, getFrontEndHandler, getAppSyncAPIDetails, getAppSyncAPIInfoFromProject } = require('../utils'); const { generateTypes: generateTypesHelper } = require('@aws-amplify/graphql-generator'); const { extractDocumentFromJavascript } = require('@aws-amplify/graphql-types-generator'); @@ -22,9 +22,18 @@ async function generateTypes(context, forceDownloadSchema, withoutInit = false, if (frontend !== 'android') { const config = loadConfig(context, withoutInit); const projects = config.getProjects(); + if (!projects.length && withoutInit) { + context.print.info(constants.ERROR_CODEGEN_NO_API_CONFIGURED); + return; + } let apis = []; if (!withoutInit) { apis = getAppSyncAPIDetails(context); + } else { + const api = await getAppSyncAPIInfoFromProject(context, projects[0]); + if (api) { + apis = [api]; + } } if (!projects.length || !apis.length) { if (!withoutInit) { @@ -48,7 +57,7 @@ async function generateTypes(context, forceDownloadSchema, withoutInit = false, const target = cfg.amplifyExtension.codeGenTarget; const excludes = cfg.excludes.map(pattern => `!${pattern}`); - const queries = glob + const queryFiles = glob .sync([...includeFiles, ...excludes], { cwd: projectPath, absolute: true, @@ -64,14 +73,22 @@ async function generateTypes(context, forceDownloadSchema, withoutInit = false, return extractDocumentFromJavascript(fileContents, ''); } return fileContents; - }) - .join('\n'); + }); + if (queryFiles.length === 0) { + throw new Error('No queries found to generate types for, you may need to run \'codegen statements\' first'); + } + const queries = queryFiles.join('\n'); const schemaPath = path.join(projectPath, cfg.schema); - let region; - if (!withoutInit) { - ({ region } = cfg.amplifyExtension); + + const outputPath = path.join(projectPath, generatedFileName); + if (apis.length) { + const { region } = cfg.amplifyExtension; await ensureIntrospectionSchema(context, schemaPath, apis[0], region, forceDownloadSchema); + } else { + if (!fs.existsSync(schemaPath)) { + throw new Error(`Cannot find GraphQL schema file: ${schemaPath}`); + } } const codeGenSpinner = new Ora(constants.INFO_MESSAGE_CODEGEN_GENERATE_STARTED); codeGenSpinner.start(); diff --git a/packages/amplify-codegen/src/constants.js b/packages/amplify-codegen/src/constants.js index 3a6cb077d..111122e6e 100644 --- a/packages/amplify-codegen/src/constants.js +++ b/packages/amplify-codegen/src/constants.js @@ -19,7 +19,8 @@ module.exports = { PROMPT_MSG_SELECT_PROJECT: 'Choose the AppSync API', PROMPT_MSG_SELECT_REGION: 'Choose AWS Region', ERROR_CODEGEN_TARGET_NOT_SUPPORTED: 'is not supported by codegen plugin', - ERROR_FLUTTER_CODEGEN_NOT_SUPPORTED: 'Flutter only supports the command $amplify codegen models. All the other codegen commands are not supported.', + ERROR_FLUTTER_CODEGEN_NOT_SUPPORTED: + 'Flutter only supports the command $amplify codegen models. All the other codegen commands are not supported.', ERROR_CODEGEN_FRONTEND_NOT_SUPPORTED: 'The project frontend is not supported by codegen', ERROR_MSG_MAX_DEPTH: 'Depth should be a integer greater than 0', ERROR_CODEGEN_NO_API_AVAILABLE: 'There are no GraphQL APIs available.\nAdd by running $amplify api add', @@ -37,7 +38,8 @@ module.exports = { CMD_DESCRIPTION_CONFIGURE: 'Change/Update codegen configuration', ERROR_CODEGEN_NO_API_CONFIGURED: 'code generation is not configured. Configure it by running \n$amplify codegen add', ERROR_CODEGEN_PENDING_API_PUSH: 'AppSync API is not pushed to the cloud. Did you forget to do \n$amplify api push', - ERROR_CODEGEN_NO_API_META: 'Cannot find API metadata. Please reset codegen by running $amplify codegen remove && amplify codegen add --apiId YOUR_API_ID', + ERROR_CODEGEN_NO_API_META: + 'Cannot find API metadata. Please reset codegen by running $amplify codegen remove && amplify codegen add --apiId YOUR_API_ID', WARNING_CODEGEN_PENDING_API_PUSH: 'The APIs listed below are not pushed to the cloud. Run amplify api push', ERROR_APPSYNC_API_NOT_FOUND: 'Could not find the AppSync API. If you have removed the AppSync API in the console run amplify codegen remove', @@ -55,5 +57,6 @@ module.exports = { INFO_MESSAGE_DOWNLOAD_ERROR: 'Downloading schema failed', INFO_MESSAGE_OPS_GEN: 'Generating GraphQL operations', INFO_MESSAGE_OPS_GEN_SUCCESS: 'Generated GraphQL operations successfully and saved at ', - INFO_MESSAGE_ADD_ERROR: 'amplify codegen add takes only apiId as parameter. \n$ amplify codegen add [--apiId ]', + INFO_MESSAGE_ADD_ERROR: + 'amplify codegen add takes only apiId and region as parameters. \n$ amplify codegen add [--apiId ] [--region ]', }; diff --git a/packages/amplify-codegen/src/utils/downloadIntrospectionSchema.js b/packages/amplify-codegen/src/utils/downloadIntrospectionSchema.js index 5fd3bb880..240eeebe7 100644 --- a/packages/amplify-codegen/src/utils/downloadIntrospectionSchema.js +++ b/packages/amplify-codegen/src/utils/downloadIntrospectionSchema.js @@ -16,7 +16,11 @@ async function downloadIntrospectionSchema(context, apiId, downloadLocation, reg const introspectionDir = dirname(downloadLocation); fs.ensureDirSync(introspectionDir); fs.writeFileSync(downloadLocation, schema, 'utf8'); - return relative(amplify.getEnvInfo().projectPath, downloadLocation); + try { + return relative(amplify.getEnvInfo().projectPath, downloadLocation); + } catch { + return downloadLocation; + } } catch (ex) { if (ex.code === 'NotFoundException') { throw new AmplifyCodeGenAPINotFoundError(constants.ERROR_APPSYNC_API_NOT_FOUND); diff --git a/packages/amplify-codegen/src/utils/ensureIntrospectionSchema.js b/packages/amplify-codegen/src/utils/ensureIntrospectionSchema.js index fc74b6633..f765d3f11 100644 --- a/packages/amplify-codegen/src/utils/ensureIntrospectionSchema.js +++ b/packages/amplify-codegen/src/utils/ensureIntrospectionSchema.js @@ -4,9 +4,12 @@ const generateIntrospectionSchema = require('./generateIntrospectionSchema'); const downloadIntrospectionSchemaWithProgress = require('./generateIntrospectionSchemaWithProgress'); async function ensureIntrospectionSchema(context, schemaPath, apiConfig, region, forceDownloadSchema) { - const meta = context.amplify.getProjectMeta(); + let meta; + try { + meta = context.amplify.getProjectMeta(); + } catch {} const { id, name } = apiConfig; - const isTransformedAPI = Object.keys(meta.api || {}).includes(name) && meta.api[name].providerPlugin === 'awscloudformation'; + const isTransformedAPI = meta && Object.keys(meta.api || {}).includes(name) && meta.api[name].providerPlugin === 'awscloudformation'; if (isTransformedAPI && getFrontendHandler(context) === 'android') { generateIntrospectionSchema(context, name); } else if (schemaPath.endsWith('.json')) { diff --git a/packages/amplify-codegen/src/utils/getAppSyncAPIInfoFromProject.js b/packages/amplify-codegen/src/utils/getAppSyncAPIInfoFromProject.js new file mode 100644 index 000000000..083f54941 --- /dev/null +++ b/packages/amplify-codegen/src/utils/getAppSyncAPIInfoFromProject.js @@ -0,0 +1,15 @@ +const getAppSyncAPIInfo = require('./getAppSyncAPIInfo'); + +/* Get AppSync api info if api id and region are avialable. + * Otherwise return undefined. + */ +async function getAppSyncAPIInfoFromProject(context, project) { + if (project.amplifyExtension.apiId && project.amplifyExtension.region) { + const { + amplifyExtension: { apiId, region }, + } = project; + return getAppSyncAPIInfo(context, apiId, region); + } + return undefined; +} +module.exports = getAppSyncAPIInfoFromProject; diff --git a/packages/amplify-codegen/src/utils/index.js b/packages/amplify-codegen/src/utils/index.js index 184cffd74..35dbbbbb8 100644 --- a/packages/amplify-codegen/src/utils/index.js +++ b/packages/amplify-codegen/src/utils/index.js @@ -5,6 +5,7 @@ const downloadIntrospectionSchema = require('./downloadIntrospectionSchema'); const getSchemaDownloadLocation = require('./getSchemaDownloadLocation'); const getIncludePattern = require('./getIncludePattern'); const getAppSyncAPIInfo = require('./getAppSyncAPIInfo'); +const getAppSyncAPIInfoFromProject = require('./getAppSyncAPIInfoFromProject'); const getProjectAwsRegion = require('./getProjectAWSRegion'); const getGraphQLDocPath = require('./getGraphQLDocPath'); const downloadIntrospectionSchemaWithProgress = require('./generateIntrospectionSchemaWithProgress'); @@ -25,6 +26,7 @@ module.exports = { downloadIntrospectionSchemaWithProgress, getIncludePattern, getAppSyncAPIInfo, + getAppSyncAPIInfoFromProject, getProjectAwsRegion, getGraphQLDocPath, isAppSyncApiPendingPush, diff --git a/packages/amplify-codegen/tests/cli/add.test.js b/packages/amplify-codegen/tests/cli/add.test.js new file mode 100644 index 000000000..5a460920f --- /dev/null +++ b/packages/amplify-codegen/tests/cli/add.test.js @@ -0,0 +1,144 @@ +const add = require('../../commands/codegen/add'); +const codeGen = require('../../src/index'); + +jest.mock('../../src/index'); + +describe('cli - add', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('feature name', () => { + expect(add.name).toEqual('add'); + }); + + describe('run', () => { + test('executes codegen add', async () => { + const context = { + parameters: {}, + }; + await add.run(context); + expect(codeGen.add).toHaveBeenCalledWith(context, null, undefined); + }); + + test('catches error in codegen add', async () => { + const error = jest.fn(); + const context = { + parameters: {}, + print: { + error, + }, + }; + + const codegenError = new Error('failed to read file'); + codeGen.add.mockRejectedValueOnce(codegenError); + await add.run(context); + expect(error).toHaveBeenCalledWith(codegenError.message); + }); + + test('passes apiId', async () => { + const apiId = 'apiid'; + const context = { + parameters: { + options: { + apiId, + }, + }, + }; + await add.run(context); + expect(codeGen.add).toHaveBeenCalledWith(context, apiId, undefined); + }); + + test('passes region', async () => { + const region = 'region'; + const context = { + parameters: { + options: { + region, + }, + }, + }; + await add.run(context); + expect(codeGen.add).toHaveBeenCalledWith(context, null, region); + }); + + test('throws error on invalid arg', async () => { + const badArg = 'badArg'; + const info = jest.fn(); + const context = { + parameters: { + options: { + badArg, + }, + }, + print: { + info, + }, + }; + await add.run(context); + expect(info).toHaveBeenCalledWith('Invalid parameter badArg'); + + expect(info).toHaveBeenCalledWith( + 'amplify codegen add takes only apiId and region as parameters. \n$ amplify codegen add [--apiId ] [--region ]', + ); + }); + + test('throws error on invalid args', async () => { + const badArgOne = 'badArgOne'; + const badArgTwo = 'badArgTwo'; + const info = jest.fn(); + const context = { + parameters: { + options: { + badArgOne, + badArgTwo, + }, + }, + print: { + info, + }, + }; + await add.run(context); + expect(info).toHaveBeenCalledWith('Invalid parameters badArgOne, badArgTwo'); + + expect(info).toHaveBeenCalledWith( + 'amplify codegen add takes only apiId and region as parameters. \n$ amplify codegen add [--apiId ] [--region ]', + ); + }); + + test('allows undocummented frontend and framework', async () => { + const frontend = 'frontend'; + const framework = 'framework'; + const info = jest.fn(); + const context = { + parameters: { + options: { + frontend, + framework, + }, + }, + print: { + info, + }, + }; + await add.run(context); + expect(info).not.toHaveBeenCalled(); + }); + + test('ignores yes arg', async () => { + const yes = true; + const info = jest.fn(); + const context = { + parameters: { + options: { + yes, + }, + }, + print: { + info, + }, + }; + await add.run(context); + }); + }); +}); diff --git a/packages/amplify-codegen/tests/commands/__snapshots__/add.test.js.snap b/packages/amplify-codegen/tests/commands/__snapshots__/add.test.js.snap new file mode 100644 index 000000000..beb912eb0 --- /dev/null +++ b/packages/amplify-codegen/tests/commands/__snapshots__/add.test.js.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`command - add without init should download introspection schema when api id 1`] = ` +Object { + "amplifyExtension": Object { + "apiId": "MOCK_API_ID", + "codeGenTarget": "TYPE_SCRIPT_OR_FLOW_OR_ANY_OTHER_LANGUAGE", + "docsFilePath": "MOCK_DOCS_FILE_PATH", + "framework": "react", + "frontend": "javascript", + "generatedFileName": "API.TS", + "region": "us-east-1", + }, + "excludes": "MOCK_EXCLUDE", + "includes": "MOCK_INCLUDE", + "projectName": "Codegen Project", + "schema": "/user/foo/project/schema.json", +} +`; + +exports[`command - add without init should read frontend and framework from options 1`] = ` +Object { + "amplifyExtension": Object { + "apiId": "MOCK_API_ID", + "codeGenTarget": "TYPE_SCRIPT_OR_FLOW_OR_ANY_OTHER_LANGUAGE", + "docsFilePath": "MOCK_DOCS_FILE_PATH", + "framework": "vue", + "frontend": "javascript", + "generatedFileName": "API.TS", + "region": "us-east-1", + }, + "excludes": "MOCK_EXCLUDE", + "includes": "MOCK_INCLUDE", + "projectName": "Codegen Project", + "schema": "/user/foo/project/schema.json", +} +`; + +exports[`command - add without init should use existing schema if no api id 1`] = ` +Object { + "amplifyExtension": Object { + "apiId": null, + "codeGenTarget": "TYPE_SCRIPT_OR_FLOW_OR_ANY_OTHER_LANGUAGE", + "docsFilePath": "MOCK_DOCS_FILE_PATH", + "framework": "react", + "frontend": "javascript", + "generatedFileName": "API.TS", + "region": "us-east-1", + }, + "excludes": "MOCK_EXCLUDE", + "includes": "MOCK_INCLUDE", + "projectName": "Codegen Project", + "schema": "/user/foo/project/schema.graphql", +} +`; + +exports[`command - add without init should use region supplied when without init 1`] = ` +Object { + "amplifyExtension": Object { + "apiId": "MOCK_API_ID", + "codeGenTarget": "TYPE_SCRIPT_OR_FLOW_OR_ANY_OTHER_LANGUAGE", + "docsFilePath": "MOCK_DOCS_FILE_PATH", + "framework": "react", + "frontend": "javascript", + "generatedFileName": "API.TS", + "region": "us-west-2", + }, + "excludes": "MOCK_EXCLUDE", + "includes": "MOCK_INCLUDE", + "projectName": "Codegen Project", + "schema": "/user/foo/project/schema.json", +} +`; diff --git a/packages/amplify-codegen/tests/commands/add.test.js b/packages/amplify-codegen/tests/commands/add.test.js index 5e921f4f3..f353d6548 100644 --- a/packages/amplify-codegen/tests/commands/add.test.js +++ b/packages/amplify-codegen/tests/commands/add.test.js @@ -1,13 +1,23 @@ +const fs = require('fs'); +const path = require('path'); const { loadConfig } = require('../../src/codegen-config'); const generateStatements = require('../../src/commands/statements'); const generateTypes = require('../../src/commands/types'); const addWalkthrough = require('../../src/walkthrough/add'); +const askForFrontend = require('../../src/walkthrough/questions/selectFrontend'); +const askForFramework = require('../../src/walkthrough/questions/selectFramework'); const changeAppSyncRegions = require('../../src/walkthrough/changeAppSyncRegions'); const { AmplifyCodeGenAPINotFoundError } = require('../../src/errors'); const add = require('../../src/commands/add'); -const { getAppSyncAPIDetails, getAppSyncAPIInfo, getProjectAwsRegion, getSDLSchemaLocation } = require('../../src/utils'); +const { + getAppSyncAPIDetails, + getAppSyncAPIInfo, + getProjectAwsRegion, + getSDLSchemaLocation, + downloadIntrospectionSchemaWithProgress, +} = require('../../src/utils'); const MOCK_CONTEXT = { print: { @@ -16,13 +26,23 @@ const MOCK_CONTEXT = { amplify: { getProjectMeta: jest.fn(), }, + parameters: { + options: {}, + }, }; +const mockProjectDir = '/user/foo/project'; +jest.mock('fs'); jest.mock('../../src/walkthrough/add'); +jest.mock('../../src/walkthrough/questions/selectFrontend'); +jest.mock('../../src/walkthrough/questions/selectFramework'); jest.mock('../../src/walkthrough/changeAppSyncRegions'); jest.mock('../../src/commands/types'); jest.mock('../../src/commands/statements'); jest.mock('../../src/codegen-config'); jest.mock('../../src/utils'); +jest.mock('process', () => ({ + cwd: () => mockProjectDir, +})); const MOCK_INCLUDE_PATTERN = 'MOCK_INCLUDE'; const MOCK_EXCLUDE_PATTERN = 'MOCK_EXCLUDE'; @@ -68,6 +88,7 @@ describe('command - add', () => { loadConfig.mockReturnValue(LOAD_CONFIG_METHODS); getProjectAwsRegion.mockReturnValue(MOCK_AWS_REGION); getSDLSchemaLocation.mockReturnValue(MOCK_SCHEMA_FILE_LOCATION); + downloadIntrospectionSchemaWithProgress.mockReturnValue(); }); it('should walkthrough add questions', async () => { @@ -148,4 +169,115 @@ describe('command - add', () => { await add(MOCK_CONTEXT); expect(generateTypes).not.toHaveBeenCalled(); }); + + it('should ignore region supplied when with init', async () => { + const region = 'us-west-2'; + await add(MOCK_CONTEXT, MOCK_API_ID, region); + expect(getProjectAwsRegion).toHaveBeenCalled(); + expect(getAppSyncAPIInfo).toHaveBeenCalledWith(MOCK_CONTEXT, MOCK_API_ID, MOCK_AWS_REGION); + }); + + describe('without init', () => { + const getProjectMeta = jest.fn(); + const schemaPath = path.join(mockProjectDir, 'schema.json'); + beforeEach(() => { + loadConfig.mockReturnValue({ ...LOAD_CONFIG_METHODS, getProjects: jest.fn().mockReturnValue([]) }); + askForFrontend.mockReturnValue('javascript'); + askForFramework.mockReturnValue('react'); + getProjectMeta.mockRejectedValue('no init'); + fs.existsSync.mockReturnValue(false); + getAppSyncAPIInfo.mockReturnValue(MOCK_APPSYNC_API_DETAIL); + downloadIntrospectionSchemaWithProgress.mockReturnValue(schemaPath); + }); + + afterEach(() => { + loadConfig.mockReset(); + askForFrontend.mockReset(); + askForFramework.mockReset(); + getProjectMeta.mockReset(); + fs.existsSync.mockReset(); + getAppSyncAPIInfo.mockReset(); + downloadIntrospectionSchemaWithProgress.mockReset(); + }); + + it('should download introspection schema when api id', async () => { + const context = { ...MOCK_CONTEXT, amplify: { getProjectMeta } }; + const defaultRegion = 'us-east-1'; + await add(context, MOCK_API_ID); + expect(getAppSyncAPIInfo).toHaveBeenCalledWith(context, MOCK_API_ID, defaultRegion); + expect(downloadIntrospectionSchemaWithProgress).toHaveBeenCalledWith(context, MOCK_API_ID, schemaPath, defaultRegion); + expect(LOAD_CONFIG_METHODS.addProject.mock.calls[0][0]).toMatchSnapshot(); + }); + + it('should use existing schema if no api id', async () => { + fs.existsSync.mockReturnValue(true); + const context = { ...MOCK_CONTEXT, amplify: { getProjectMeta } }; + const defaultRegion = 'us-east-1'; + await add(context); + expect(getAppSyncAPIInfo).not.toHaveBeenCalled(); + expect(downloadIntrospectionSchemaWithProgress).not.toHaveBeenCalled(); + expect(LOAD_CONFIG_METHODS.addProject.mock.calls[0][0]).toMatchSnapshot(); + }); + + it('should read frontend and framework from options', async () => { + const parameters = { + options: { + frontend: 'javascript', + framework: 'vue', + }, + }; + await add({ ...MOCK_CONTEXT, amplify: { getProjectMeta }, parameters }, MOCK_API_ID); + expect(askForFrontend).not.toHaveBeenCalled(); + expect(askForFramework).not.toHaveBeenCalled(); + expect(LOAD_CONFIG_METHODS.addProject).toHaveBeenCalled(); + expect(LOAD_CONFIG_METHODS.addProject.mock.calls[0][0]).toMatchSnapshot(); + }); + + it('should use region supplied when without init', async () => { + const region = 'us-west-2'; + const context = { ...MOCK_CONTEXT, amplify: { getProjectMeta } }; + await add(context, MOCK_API_ID, region); + expect(getProjectAwsRegion).not.toHaveBeenCalled(); + expect(getAppSyncAPIInfo).toHaveBeenCalledWith(context, MOCK_API_ID, region); + expect(LOAD_CONFIG_METHODS.addProject).toHaveBeenCalled(); + expect(LOAD_CONFIG_METHODS.addProject.mock.calls[0][0]).toMatchSnapshot(); + }); + + it('should error on invalid frontend', () => { + const parameters = { + options: { + frontend: 'foo', + }, + }; + expect(add({ ...MOCK_CONTEXT, amplify: { getProjectMeta }, parameters }, MOCK_API_ID)).rejects.toThrowError( + 'Invalid frontend provided', + ); + }); + + it('should error on invalid framework', () => { + const parameters = { + options: { + frontend: 'javascript', + framework: 'foo', + }, + }; + expect(add({ ...MOCK_CONTEXT, amplify: { getProjectMeta }, parameters }, MOCK_API_ID)).rejects.toThrowError( + 'Invalid framework provided', + ); + }); + + it('should error if codegen project already exists', () => { + loadConfig.mockReturnValue({ ...LOAD_CONFIG_METHODS, getProjects: jest.fn().mockReturnValue(['foo']) }); + expect(add({ ...MOCK_CONTEXT, amplify: { getProjectMeta } }, MOCK_API_ID)).rejects.toThrowError( + 'Codegen support only one GraphQL API per project', + ); + }); + + it('should error if codegen project already exists', () => { + fs.existsSync.mockReturnValue(false); + expect(add({ ...MOCK_CONTEXT, amplify: { getProjectMeta } })).rejects.toThrowError( + 'Provide an AppSync API ID with --apiId or manually download schema.graphql or schema.json and place in /user/foo/project before adding codegen when not in an amplify project', + ); + }); + }); });