diff --git a/packages/amplify-codegen/src/commands/add.js b/packages/amplify-codegen/src/commands/add.js index c81546d66..6b3a609a9 100644 --- a/packages/amplify-codegen/src/commands/add.js +++ b/packages/amplify-codegen/src/commands/add.js @@ -38,9 +38,9 @@ async function add(context, apiId = null, region = 'us-east-1') { } 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 @@ -78,36 +78,37 @@ async function add(context, apiId = null, region = 'us-east-1') { 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, region = 'us-east-1') { } 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/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/tests/commands/__snapshots__/add.test.js.snap b/packages/amplify-codegen/tests/commands/__snapshots__/add.test.js.snap index 5cbd750a3..beb912eb0 100644 --- a/packages/amplify-codegen/tests/commands/__snapshots__/add.test.js.snap +++ b/packages/amplify-codegen/tests/commands/__snapshots__/add.test.js.snap @@ -1,5 +1,23 @@ // 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 { @@ -14,6 +32,24 @@ Object { "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", } `; @@ -32,6 +68,6 @@ Object { "excludes": "MOCK_EXCLUDE", "includes": "MOCK_INCLUDE", "projectName": "Codegen Project", - "schema": "/user/foo/project/schema.graphql", + "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 b1e7a1f40..f353d6548 100644 --- a/packages/amplify-codegen/tests/commands/add.test.js +++ b/packages/amplify-codegen/tests/commands/add.test.js @@ -1,4 +1,5 @@ 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'); @@ -10,7 +11,13 @@ 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: { @@ -23,6 +30,7 @@ const MOCK_CONTEXT = { options: {}, }, }; +const mockProjectDir = '/user/foo/project'; jest.mock('fs'); jest.mock('../../src/walkthrough/add'); jest.mock('../../src/walkthrough/questions/selectFrontend'); @@ -33,7 +41,7 @@ jest.mock('../../src/commands/statements'); jest.mock('../../src/codegen-config'); jest.mock('../../src/utils'); jest.mock('process', () => ({ - cwd: () => '/user/foo/project', + cwd: () => mockProjectDir, })); const MOCK_INCLUDE_PATTERN = 'MOCK_INCLUDE'; @@ -80,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 () => { @@ -170,12 +179,15 @@ describe('command - add', () => { 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(true); + fs.existsSync.mockReturnValue(false); + getAppSyncAPIInfo.mockReturnValue(MOCK_APPSYNC_API_DETAIL); + downloadIntrospectionSchemaWithProgress.mockReturnValue(schemaPath); }); afterEach(() => { @@ -184,6 +196,27 @@ describe('command - add', () => { 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 () => { @@ -202,8 +235,10 @@ describe('command - add', () => { it('should use region supplied when without init', async () => { const region = 'us-west-2'; - await add({ ...MOCK_CONTEXT, amplify: { getProjectMeta } }, MOCK_API_ID, region); + 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(); }); @@ -230,5 +265,19 @@ describe('command - add', () => { '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', + ); + }); }); });