From 15685810460881aa71e88724398b9be05ba53781 Mon Sep 17 00:00:00 2001 From: Albert Winberg Date: Fri, 30 Jun 2023 05:52:13 +0000 Subject: [PATCH] feat: generate components using graphql --- package.json | 2 +- .../amplify-appsync-simulator/package.json | 2 +- packages/amplify-category-auth/package.json | 2 +- .../amplify-category-function/package.json | 2 +- packages/amplify-category-geo/package.json | 2 +- .../package.json | 2 +- .../amplify-category-predictions/package.json | 2 +- .../amplify-category-storage/package.json | 2 +- packages/amplify-cli-npm/index.ts | 2 +- packages/amplify-cli/package.json | 2 +- .../package.json | 2 +- .../amplify-dynamodb-simulator/package.json | 2 +- packages/amplify-e2e-core/package.json | 2 +- packages/amplify-e2e-tests/package.json | 2 +- .../package.json | 2 +- .../amplify-opensearch-simulator/package.json | 2 +- .../package.json | 2 +- .../amplify-storage-simulator/package.json | 2 +- packages/amplify-util-import/package.json | 2 +- packages/amplify-util-mock/package.json | 2 +- packages/amplify-util-uibuilder/package.json | 2 +- .../src/__tests__/generateComponents.test.ts | 257 +++++++++++------- .../utils/getApiConfiguration.test.ts | 41 +++ .../src/clients/amplify-studio-client.ts | 3 + .../src/commands/generateComponents.ts | 19 +- .../src/commands/utils/getApiConfiguration.ts | 73 +++++ .../types/amplify-codegen.d.ts | 12 + yarn.lock | 46 ++-- 28 files changed, 351 insertions(+), 142 deletions(-) create mode 100644 packages/amplify-util-uibuilder/src/__tests__/utils/getApiConfiguration.test.ts create mode 100644 packages/amplify-util-uibuilder/src/commands/utils/getApiConfiguration.ts create mode 100644 packages/amplify-util-uibuilder/types/amplify-codegen.d.ts diff --git a/package.json b/package.json index 6b34bf06703..175c2f711a2 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ }, "packageManager": "yarn@3.5.0", "resolutions": { - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "cross-fetch": "^2.2.6", "glob-parent": "^6.0.2", "got": "^11.8.5", diff --git a/packages/amplify-appsync-simulator/package.json b/packages/amplify-appsync-simulator/package.json index 2db22a30ceb..511efdfb387 100644 --- a/packages/amplify-appsync-simulator/package.json +++ b/packages/amplify-appsync-simulator/package.json @@ -35,7 +35,7 @@ "@graphql-tools/schema": "^8.3.1", "@graphql-tools/utils": "^8.5.1", "amplify-velocity-template": "1.4.12", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "chalk": "^4.1.1", "cors": "^2.8.5", "dataloader": "^2.0.0", diff --git a/packages/amplify-category-auth/package.json b/packages/amplify-category-auth/package.json index 23141e2b8db..4cf54f600af 100644 --- a/packages/amplify-category-auth/package.json +++ b/packages/amplify-category-auth/package.json @@ -37,7 +37,7 @@ "amplify-headless-interface": "1.17.4", "amplify-util-headless-input": "1.9.14", "aws-cdk-lib": "~2.80.0", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "axios": "^0.26.0", "chalk": "^4.1.1", "change-case": "^4.1.1", diff --git a/packages/amplify-category-function/package.json b/packages/amplify-category-function/package.json index e55cfe96ef5..2e152d0f905 100644 --- a/packages/amplify-category-function/package.json +++ b/packages/amplify-category-function/package.json @@ -31,7 +31,7 @@ "@aws-amplify/amplify-function-plugin-interface": "1.11.0", "@aws-amplify/amplify-prompts": "2.8.1", "archiver": "^5.3.0", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "chalk": "^4.1.1", "cloudform-types": "^4.2.0", "enquirer": "^2.3.6", diff --git a/packages/amplify-category-geo/package.json b/packages/amplify-category-geo/package.json index ba103f3afd0..c04e2d2ff2d 100644 --- a/packages/amplify-category-geo/package.json +++ b/packages/amplify-category-geo/package.json @@ -32,7 +32,7 @@ "amplify-headless-interface": "1.17.4", "amplify-util-headless-input": "1.9.14", "aws-cdk-lib": "~2.80.0", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "constructs": "^10.0.5", "fs-extra": "^8.1.0", "lodash": "^4.17.21", diff --git a/packages/amplify-category-notifications/package.json b/packages/amplify-category-notifications/package.json index 603aab5bf9b..67db92954eb 100644 --- a/packages/amplify-category-notifications/package.json +++ b/packages/amplify-category-notifications/package.json @@ -30,7 +30,7 @@ "@aws-amplify/amplify-environment-parameters": "1.7.4", "@aws-amplify/amplify-prompts": "2.8.1", "@aws-amplify/amplify-provider-awscloudformation": "8.4.0", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "chalk": "^4.1.1", "fs-extra": "^8.1.0", "lodash": "^4.17.21", diff --git a/packages/amplify-category-predictions/package.json b/packages/amplify-category-predictions/package.json index 282b165739c..531fa045e6e 100644 --- a/packages/amplify-category-predictions/package.json +++ b/packages/amplify-category-predictions/package.json @@ -27,7 +27,7 @@ "dependencies": { "@aws-amplify/amplify-cli-core": "4.2.4", "@aws-amplify/amplify-prompts": "2.8.1", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "chalk": "^4.1.1", "fs-extra": "^8.1.0", "uuid": "^8.3.2" diff --git a/packages/amplify-category-storage/package.json b/packages/amplify-category-storage/package.json index 6acfbf13925..0205368ca03 100644 --- a/packages/amplify-category-storage/package.json +++ b/packages/amplify-category-storage/package.json @@ -35,7 +35,7 @@ "amplify-headless-interface": "1.17.4", "amplify-util-headless-input": "1.9.14", "aws-cdk-lib": "~2.80.0", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "chalk": "^4.1.1", "constructs": "^10.0.5", "enquirer": "^2.3.6", diff --git a/packages/amplify-cli-npm/index.ts b/packages/amplify-cli-npm/index.ts index 6f49e035acb..10713d6d500 100644 --- a/packages/amplify-cli-npm/index.ts +++ b/packages/amplify-cli-npm/index.ts @@ -16,4 +16,4 @@ export const install = async (): Promise => { return binary.install(); }; -// force version bump to 12.3.0 +// force version bump to 12.4.0 diff --git a/packages/amplify-cli/package.json b/packages/amplify-cli/package.json index 20ce24a1d2c..9e1daeafc81 100644 --- a/packages/amplify-cli/package.json +++ b/packages/amplify-cli/package.json @@ -74,7 +74,7 @@ "amplify-nodejs-function-runtime-provider": "2.5.4", "amplify-python-function-runtime-provider": "2.4.27", "aws-cdk-lib": "~2.80.0", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "chalk": "^4.1.1", "ci-info": "^3.8.0", "cli-table3": "^0.6.0", diff --git a/packages/amplify-console-integration-tests/package.json b/packages/amplify-console-integration-tests/package.json index 916f80b9c3e..492c154b70b 100644 --- a/packages/amplify-console-integration-tests/package.json +++ b/packages/amplify-console-integration-tests/package.json @@ -24,7 +24,7 @@ "@aws-amplify/amplify-cli-core": "4.2.4", "@aws-amplify/amplify-e2e-core": "5.2.0", "@types/ini": "^1.3.30", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "dotenv": "^8.2.0", "execa": "^5.1.1", "fs-extra": "^8.1.0", diff --git a/packages/amplify-dynamodb-simulator/package.json b/packages/amplify-dynamodb-simulator/package.json index 4ea15ac3794..5ed3a37365a 100644 --- a/packages/amplify-dynamodb-simulator/package.json +++ b/packages/amplify-dynamodb-simulator/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@aws-amplify/amplify-cli-core": "4.2.4", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "detect-port": "^1.3.0", "execa": "^5.1.1", "fs-extra": "^8.1.0", diff --git a/packages/amplify-e2e-core/package.json b/packages/amplify-e2e-core/package.json index 903a0d1c31d..cc00632735b 100644 --- a/packages/amplify-e2e-core/package.json +++ b/packages/amplify-e2e-core/package.json @@ -28,7 +28,7 @@ "amplify-headless-interface": "1.17.4", "aws-amplify": "^4.2.8", "aws-appsync": "^4.1.1", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "chalk": "^4.1.1", "dotenv": "^8.2.0", "execa": "^5.1.1", diff --git a/packages/amplify-e2e-tests/package.json b/packages/amplify-e2e-tests/package.json index f66015df97e..0b5b83ca1dd 100644 --- a/packages/amplify-e2e-tests/package.json +++ b/packages/amplify-e2e-tests/package.json @@ -40,7 +40,7 @@ "aws-amplify": "^4.2.8", "aws-appsync": "^4.1.1", "aws-cdk-lib": "~2.80.0", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "axios": "^0.26.0", "circleci-api": "^4.1.4", "constructs": "^10.0.5", diff --git a/packages/amplify-environment-parameters/package.json b/packages/amplify-environment-parameters/package.json index d9eb3f7bb36..eaa5e107fc4 100644 --- a/packages/amplify-environment-parameters/package.json +++ b/packages/amplify-environment-parameters/package.json @@ -32,7 +32,7 @@ "lodash": "^4.17.21" }, "devDependencies": { - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "mkdirp": "^1.0.4", "ts-json-schema-generator": "~1.1.2" }, diff --git a/packages/amplify-opensearch-simulator/package.json b/packages/amplify-opensearch-simulator/package.json index 97cd6474a30..3995eea2605 100644 --- a/packages/amplify-opensearch-simulator/package.json +++ b/packages/amplify-opensearch-simulator/package.json @@ -27,7 +27,7 @@ "dependencies": { "@aws-amplify/amplify-cli-core": "4.2.4", "@aws-amplify/amplify-prompts": "2.8.1", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "detect-port": "^1.3.0", "execa": "^5.1.1", "fs-extra": "^8.1.0", diff --git a/packages/amplify-provider-awscloudformation/package.json b/packages/amplify-provider-awscloudformation/package.json index 6f2d950c751..a56f53ccb42 100644 --- a/packages/amplify-provider-awscloudformation/package.json +++ b/packages/amplify-provider-awscloudformation/package.json @@ -40,7 +40,7 @@ "amplify-codegen": "^4.2.0", "archiver": "^5.3.0", "aws-cdk-lib": "~2.80.0", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "bottleneck": "2.19.5", "chalk": "^4.1.1", "cloudform-types": "^4.2.0", diff --git a/packages/amplify-storage-simulator/package.json b/packages/amplify-storage-simulator/package.json index 06e043c42e3..8af60b5609f 100644 --- a/packages/amplify-storage-simulator/package.json +++ b/packages/amplify-storage-simulator/package.json @@ -44,7 +44,7 @@ "@types/serve-static": "^1.13.3", "@types/uuid": "^8.3.1", "@types/xml": "^1.0.4", - "aws-sdk": "^2.1405.0" + "aws-sdk": "^2.1426.0" }, "berry": { "plugins": [ diff --git a/packages/amplify-util-import/package.json b/packages/amplify-util-import/package.json index abff2c5e9b8..937b66c75a4 100644 --- a/packages/amplify-util-import/package.json +++ b/packages/amplify-util-import/package.json @@ -20,7 +20,7 @@ "author": "Amazon Web Services", "license": "Apache-2.0", "dependencies": { - "aws-sdk": "^2.1405.0" + "aws-sdk": "^2.1426.0" }, "devDependencies": { "@types/node": "^12.12.6" diff --git a/packages/amplify-util-mock/package.json b/packages/amplify-util-mock/package.json index 7e9bd6fb479..915427b3eb3 100644 --- a/packages/amplify-util-mock/package.json +++ b/packages/amplify-util-mock/package.json @@ -76,7 +76,7 @@ "@types/which": "^1.3.2", "amplify-nodejs-function-runtime-provider": "2.5.4", "aws-appsync": "^4.1.4", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "aws-sdk-mock": "^5.8.0", "axios": "^0.26.0", "graphql": "^15.5.0", diff --git a/packages/amplify-util-uibuilder/package.json b/packages/amplify-util-uibuilder/package.json index 0a4d45bd9de..352a3931e76 100644 --- a/packages/amplify-util-uibuilder/package.json +++ b/packages/amplify-util-uibuilder/package.json @@ -20,7 +20,7 @@ "@aws-amplify/codegen-ui": "2.14.2", "@aws-amplify/codegen-ui-react": "2.14.2", "amplify-codegen": "^4.2.0", - "aws-sdk": "^2.1405.0", + "aws-sdk": "^2.1426.0", "fs-extra": "^8.1.0", "node-fetch": "^2.6.7", "ora": "^4.0.3", diff --git a/packages/amplify-util-uibuilder/src/__tests__/generateComponents.test.ts b/packages/amplify-util-uibuilder/src/__tests__/generateComponents.test.ts index 69d937987c4..493c7a6d1af 100644 --- a/packages/amplify-util-uibuilder/src/__tests__/generateComponents.test.ts +++ b/packages/amplify-util-uibuilder/src/__tests__/generateComponents.test.ts @@ -3,6 +3,8 @@ import * as utils from '../commands/utils'; import { run } from '../commands/generateComponents'; import { isDataStoreEnabled } from '@aws-amplify/amplify-category-api'; import { getTransformerVersion } from '../commands/utils/featureFlags'; +import { getCodegenConfig } from 'amplify-codegen'; +import { getUiBuilderComponentsPath } from '../commands/utils/getUiBuilderComponentsPath'; jest.mock('../commands/utils'); jest.mock('@aws-amplify/amplify-cli-core'); @@ -14,11 +16,22 @@ jest.mock('../commands/utils/featureFlags', () => ({ ...jest.requireActual('../commands/utils/featureFlags'), getTransformerVersion: jest.fn(), })); +jest.mock('../commands/utils/getUiBuilderComponentsPath', () => ({ + ...jest.requireActual('../commands/utils/getUiBuilderComponentsPath'), + getUiBuilderComponentsPath: jest.fn(), +})); +jest.mock('amplify-codegen', () => ({ + ...jest.requireActual('amplify-codegen'), + getCodegenConfig: jest.fn(), +})); + const awsMock = aws as any; const utilsMock = utils as any; +const isDataStoreEnabledMocked = isDataStoreEnabled as any; +const getTransformerVersionMocked = getTransformerVersion as any; +const getCodegenConfigMocked = getCodegenConfig as any; +const getUiBuilderComponentsPathMocked = getUiBuilderComponentsPath as any; -const isDataStoreEnabledMocked = jest.mocked(isDataStoreEnabled); -const getTransformerVersionMocked = jest.mocked(getTransformerVersion); utilsMock.shouldRenderComponents = jest.fn().mockReturnValue(true); utilsMock.notifyMissingPackages = jest.fn().mockReturnValue(true); utilsMock.getAmplifyDataSchema = jest.fn().mockReturnValue({}); @@ -32,18 +45,23 @@ jest.mock('../commands/utils/featureFlags', () => ({ getTransformerVersion: jest.fn().mockReturnValue(2), })); +const defaultStudioFeatureFlags = { + autoGenerateForms: 'true', + autoGenerateViews: 'true', + isRelationshipSupported: 'false', + isNonModelSupported: 'false', + isGraphQLEnabled: 'true', +}; + +const projectPath = '/usr/test/test-project'; + describe('can generate components', () => { let context: any; let schemas: any; let mockedExport: jest.Mock; const getMetadataPromise = jest.fn().mockReturnValue({ features: { - autoGenerateForms: 'true', - autoGenerateViews: 'true', - formFeatureFlags: { - isRelationshipSupported: 'false', - isNonModelSupported: 'false', - }, + ...defaultStudioFeatureFlags, }, }); const startCodegenJobPromise = jest.fn().mockReturnValue({ @@ -53,6 +71,7 @@ describe('can generate components', () => { promise: startCodegenJobPromise, }); beforeEach(() => { + jest.clearAllMocks(); isDataStoreEnabledMocked.mockResolvedValue(true); getTransformerVersionMocked.mockResolvedValue(2); context = { @@ -65,6 +84,11 @@ describe('can generate components', () => { envName: 'testEnvName', }, }, + exeInfo: { + localEnvInfo: { + projectPath, + }, + }, }; schemas = { entities: [ @@ -81,6 +105,16 @@ describe('can generate components', () => { }, ], }; + + getCodegenConfigMocked.mockReturnValue({ + getGeneratedTypesPath: jest.fn().mockReturnValue(undefined), + getGeneratedQueriesPath: jest.fn().mockReturnValue(projectPath + '/src/graphql/queries.js'), + getGeneratedMutationsPath: jest.fn().mockReturnValue(projectPath + '/src/graphql/mutations.js'), + getGeneratedSubscriptionsPath: jest.fn().mockReturnValue(projectPath + '/src/graphql/subscriptions.js'), + getGeneratedFragmentsPath: jest.fn().mockReturnValue(projectPath + '/src/graphql/fragments.js'), + getQueryMaxDepth: jest.fn().mockReturnValue(3), + }); + mockedExport = jest.fn().mockReturnValue({ entities: schemas.entities, }); @@ -105,10 +139,10 @@ describe('can generate components', () => { promise: jest.fn().mockReturnValue({ status: 'succeeded' }), }), }); + getUiBuilderComponentsPathMocked.mockReturnValue(projectPath + '/src/ui-components'); utilsMock.generateUiBuilderComponents = jest.fn().mockReturnValue(schemas.entities); utilsMock.generateUiBuilderThemes = jest.fn().mockReturnValue(schemas.entities); utilsMock.generateUiBuilderForms = jest.fn().mockReturnValue(schemas.entities); - utilsMock.getAmplifyDataSchema = jest.fn().mockReturnValue(undefined); utilsMock.generateAmplifyUiBuilderIndexFile = jest.fn().mockReturnValue(true); utilsMock.generateAmplifyUiBuilderUtilFile = jest.fn().mockReturnValue(true); utilsMock.deleteDetachedForms = jest.fn(); @@ -120,7 +154,7 @@ describe('can generate components', () => { expect(mockedExport).toBeCalledTimes(3); expect(startCodegenJobPromise).toBeCalledTimes(1); expect(utilsMock.waitForSucceededJob).toBeCalledTimes(1); - expect(utilsMock.getUiBuilderComponentsPath).toBeCalledTimes(1); + expect(getUiBuilderComponentsPathMocked).toBeCalledTimes(1); expect(utilsMock.extractUIComponents).toBeCalledTimes(1); expect(utilsMock.deleteDetachedForms).toBeCalledTimes(1); }); @@ -130,34 +164,14 @@ describe('can generate components', () => { getTransformerVersionMocked.mockResolvedValue(2); getMetadataPromise.mockReturnValue({ features: { - autoGenerateForms: 'true', - autoGenerateViews: 'true', - formFeatureFlags: { - isRelationshipSupported: 'false', - isNonModelSupported: 'false', - }, + ...defaultStudioFeatureFlags, }, }); await run(context, 'PostPull'); expect(mockStartCodegenJob).toHaveBeenCalledWith({ appId: 'testAppId', environmentName: 'testEnvName', - codegenJobToCreate: { - renderConfig: { - react: { - module: 'es2020', - target: 'es2020', - script: 'jsx', - renderTypeDeclarations: true, - }, - }, - genericDataSchema: undefined, - autoGenerateForms: true, - features: { - isNonModelSupported: false, - isRelationshipSupported: false, - }, - }, + codegenJobToCreate: expect.objectContaining({ autoGenerateForms: true }), }); }); @@ -166,104 +180,163 @@ describe('can generate components', () => { getTransformerVersionMocked.mockResolvedValue(1); getMetadataPromise.mockReturnValue({ features: { - autoGenerateForms: 'true', - autoGenerateViews: 'true', - formFeatureFlags: { - isRelationshipSupported: 'false', - isNonModelSupported: 'false', - }, + ...defaultStudioFeatureFlags, }, }); await run(context, 'PostPull'); expect(mockStartCodegenJob).toHaveBeenCalledWith({ appId: 'testAppId', environmentName: 'testEnvName', - codegenJobToCreate: { - renderConfig: { - react: { - module: 'es2020', - target: 'es2020', - script: 'jsx', - renderTypeDeclarations: true, - }, - }, - genericDataSchema: undefined, - autoGenerateForms: false, - features: { - isNonModelSupported: false, - isRelationshipSupported: false, - }, + codegenJobToCreate: expect.objectContaining({ autoGenerateForms: false }), + }); + }); + + it('should not autogenerate forms if datastore is not enabled and GraphQL is not enabled', async () => { + isDataStoreEnabledMocked.mockResolvedValue(false); + getMetadataPromise.mockReturnValue({ + features: { + ...defaultStudioFeatureFlags, + isGraphQLEnabled: 'false', }, }); + await run(context, 'PostPull'); + expect(mockStartCodegenJob).toHaveBeenCalledWith({ + appId: 'testAppId', + environmentName: 'testEnvName', + codegenJobToCreate: expect.objectContaining({ autoGenerateForms: false }), + }); }); - it('should not autogenerate forms if datastore is not enabled', async () => { + it('should not autogenerate forms if datastore is not enabled and GraphQL is enabled with invalid config', async () => { isDataStoreEnabledMocked.mockResolvedValue(false); getMetadataPromise.mockReturnValue({ features: { - autoGenerateForms: 'true', - autoGenerateViews: 'true', - formFeatureFlags: { - isRelationshipSupported: 'false', - isNonModelSupported: 'false', - }, + ...defaultStudioFeatureFlags, + isGraphQLEnabled: 'true', }, }); + getCodegenConfigMocked.mockImplementation(() => { + throw new Error(); + }); await run(context, 'PostPull'); expect(mockStartCodegenJob).toHaveBeenCalledWith({ appId: 'testAppId', environmentName: 'testEnvName', - codegenJobToCreate: { - renderConfig: { - react: { - module: 'es2020', - target: 'es2020', - script: 'jsx', - renderTypeDeclarations: true, - }, - }, - genericDataSchema: undefined, - autoGenerateForms: false, - features: { - isNonModelSupported: false, - isRelationshipSupported: false, - }, + codegenJobToCreate: expect.objectContaining({ autoGenerateForms: false }), + }); + }); + + it('should autogenerate forms if datastore is not enabled and GraphQL is enabled with valid config', async () => { + isDataStoreEnabledMocked.mockResolvedValue(false); + getMetadataPromise.mockReturnValue({ + features: { + ...defaultStudioFeatureFlags, + isGraphQLEnabled: 'true', }, }); + await run(context, 'PostPull'); + expect(mockStartCodegenJob).toHaveBeenCalledWith({ + appId: 'testAppId', + environmentName: 'testEnvName', + codegenJobToCreate: expect.objectContaining({ autoGenerateForms: true }), + }); }); it('should not autogenerate forms if feature flag is not enabled', async () => { isDataStoreEnabledMocked.mockResolvedValue(true); getMetadataPromise.mockReturnValue({ features: { + ...defaultStudioFeatureFlags, autoGenerateForms: 'false', - autoGenerateViews: 'true', - formFeatureFlags: { - isRelationshipSupported: 'false', - isNonModelSupported: 'false', - }, }, }); await run(context, 'PostPull'); expect(mockStartCodegenJob).toHaveBeenCalledWith({ appId: 'testAppId', environmentName: 'testEnvName', - codegenJobToCreate: { - renderConfig: { - react: { - module: 'es2020', - target: 'es2020', - script: 'jsx', - renderTypeDeclarations: true, + codegenJobToCreate: expect.objectContaining({ autoGenerateForms: false }), + }); + }); + + describe('codegen job creation', () => { + it('should inclue dataStore configuration when dataStore is enabled', async () => { + isDataStoreEnabledMocked.mockResolvedValue(true); + getMetadataPromise.mockReturnValue({ + features: { + ...defaultStudioFeatureFlags, + }, + }); + await run(context, 'PostPull'); + expect(mockStartCodegenJob).toHaveBeenCalledWith({ + appId: 'testAppId', + environmentName: 'testEnvName', + codegenJobToCreate: expect.objectContaining({ + renderConfig: { + react: expect.objectContaining({ + apiConfiguration: { + dataStoreConfig: {}, + }, + }), }, + }), + }); + }); + + it('should inclue GraphQL configuration when dataStore is disabled and valid api configuration is found', async () => { + isDataStoreEnabledMocked.mockResolvedValue(false); + getMetadataPromise.mockReturnValue({ + features: { + ...defaultStudioFeatureFlags, + isGraphQLEnabled: 'true', }, - genericDataSchema: undefined, - autoGenerateForms: false, + }); + await run(context, 'PostPull'); + expect(mockStartCodegenJob).toHaveBeenCalledWith({ + appId: 'testAppId', + environmentName: 'testEnvName', + codegenJobToCreate: expect.objectContaining({ + renderConfig: { + react: expect.objectContaining({ + apiConfiguration: { + graphQLConfig: { + fragmentsFilePath: '../graphql/fragments.js', + mutationsFilePath: '../graphql/mutations.js', + queriesFilePath: '../graphql/queries.js', + subscriptionsFilePath: '../graphql/subscriptions.js', + typesFilePath: '', + }, + }, + }), + }, + }), + }); + }); + + it('should inclue noApi configuration when dataStore is disabled and no valid GraphQL Api', async () => { + isDataStoreEnabledMocked.mockResolvedValue(false); + getMetadataPromise.mockReturnValue({ features: { - isNonModelSupported: false, - isRelationshipSupported: false, + ...defaultStudioFeatureFlags, + isGraphQLEnabled: 'true', }, - }, + }); + getCodegenConfigMocked.mockImplementation(() => { + throw new Error(); + }); + await run(context, 'PostPull'); + expect(mockStartCodegenJob).toHaveBeenCalledWith({ + appId: 'testAppId', + environmentName: 'testEnvName', + codegenJobToCreate: expect.objectContaining({ + renderConfig: { + react: expect.objectContaining({ + apiConfiguration: { + noApiConfig: {}, + }, + }), + }, + }), + }); }); }); }); diff --git a/packages/amplify-util-uibuilder/src/__tests__/utils/getApiConfiguration.test.ts b/packages/amplify-util-uibuilder/src/__tests__/utils/getApiConfiguration.test.ts new file mode 100644 index 00000000000..68c51c134ab --- /dev/null +++ b/packages/amplify-util-uibuilder/src/__tests__/utils/getApiConfiguration.test.ts @@ -0,0 +1,41 @@ +import { $TSContext } from '@aws-amplify/amplify-cli-core'; +import { relativeToComponentsPath } from '../../commands/utils/getApiConfiguration'; +import { getUiBuilderComponentsPath } from '../../commands/utils/getUiBuilderComponentsPath'; +import path from 'path'; + +jest.mock('../../commands/utils/getUiBuilderComponentsPath', () => ({ + ...jest.requireActual('../../commands/utils/getUiBuilderComponentsPath'), + getUiBuilderComponentsPath: jest.fn(), +})); + +jest.mock('path', () => ({ + ...jest.requireActual('path'), +})); + +const pathMocked = path as any; + +const getUiBuilderComponentsPathMocked = getUiBuilderComponentsPath as any; + +describe('relativeToComponentsPath', () => { + it('should return posix relative path when run in a windows-like environment', () => { + pathMocked.relative = path.win32.relative; + pathMocked.sep = path.win32.sep; + const projectPath = 'c:\\dev\\test\\test-project'; + const toImport = projectPath + '\\src\\graphql\\queries.js'; + getUiBuilderComponentsPathMocked.mockReturnValue(projectPath + '\\src\\ui-components'); + + const response = relativeToComponentsPath(toImport, {} as $TSContext); + + expect(response).toBe('../graphql/queries.js'); + }); + + it('should return expected relative path', () => { + const projectPath = '/dev/test/test-project'; + const toImport = projectPath + '/src/graphql/queries.js'; + getUiBuilderComponentsPathMocked.mockReturnValue(projectPath + '/src/ui-components'); + + const response = relativeToComponentsPath(toImport, {} as $TSContext); + + expect(response).toBe('../graphql/queries.js'); + }); +}); diff --git a/packages/amplify-util-uibuilder/src/clients/amplify-studio-client.ts b/packages/amplify-util-uibuilder/src/clients/amplify-studio-client.ts index 4ced90235ea..ef68d228d8a 100644 --- a/packages/amplify-util-uibuilder/src/clients/amplify-studio-client.ts +++ b/packages/amplify-util-uibuilder/src/clients/amplify-studio-client.ts @@ -17,6 +17,7 @@ export type StudioMetadata = { isRelationshipSupported: boolean; isNonModelSupported: boolean; }; + isGraphQLEnabled: boolean; }; /** @@ -122,6 +123,7 @@ export default class AmplifyStudioClient { isRelationshipSupported: false, isNonModelSupported: false, }, + isGraphQLEnabled: false, }; } @@ -145,6 +147,7 @@ export default class AmplifyStudioClient { isRelationshipSupported: response.features?.isRelationshipSupported === 'true', isNonModelSupported: response.features?.isNonModelSupported === 'true', }, + isGraphQLEnabled: response.features?.isGraphQLEnabled === 'true', }; } catch (err) { throw new Error(`Failed to load metadata: ${err.message}`); diff --git a/packages/amplify-util-uibuilder/src/commands/generateComponents.ts b/packages/amplify-util-uibuilder/src/commands/generateComponents.ts index 323c4076852..64bbcca6e50 100644 --- a/packages/amplify-util-uibuilder/src/commands/generateComponents.ts +++ b/packages/amplify-util-uibuilder/src/commands/generateComponents.ts @@ -12,10 +12,11 @@ import { hasStorageField, mapGenericDataSchemaToCodegen, waitForSucceededJob, - getUiBuilderComponentsPath, extractUIComponents, } from './utils'; +import { getUiBuilderComponentsPath } from './utils/getUiBuilderComponentsPath'; import { AmplifyUIBuilder } from 'aws-sdk'; +import { getApiConfiguration, hasDataStoreConfiguration, hasGraphQLConfiguration } from './utils/getApiConfiguration'; /** * Pulls ui components from Studio backend and generates the code in the user's file system @@ -34,10 +35,15 @@ export const run = async (context: $TSContext, eventType: 'PostPush' | 'PostPull studioClient.isGraphQLSupported ? getAmplifyDataSchema(context) : Promise.resolve(undefined), ]); - const nothingWouldAutogenerate = - !dataSchema || !studioClient.metadata.autoGenerateForms || !studioClient.isGraphQLSupported || !studioClient.isDataStoreEnabled; + const canGenerateDataComponents = dataSchema && studioClient.isGraphQLSupported; - if (nothingWouldAutogenerate && [componentSchemas, themeSchemas, formSchemas].every((group) => !group.entities.length)) { + const apiConfiguration: AmplifyUIBuilder.ApiConfiguration = canGenerateDataComponents + ? getApiConfiguration(studioClient, context) + : { noApiConfig: {} }; + const hasDataAPI = hasDataStoreConfiguration(apiConfiguration) || hasGraphQLConfiguration(apiConfiguration); + const willAutogenerateItems = canGenerateDataComponents && studioClient.metadata.autoGenerateForms && hasDataAPI; + + if (!willAutogenerateItems && [componentSchemas, themeSchemas, formSchemas].every((group) => !group.entities.length)) { printer.debug('Skipping UI component generation since none are found.'); return; } @@ -52,9 +58,10 @@ export const run = async (context: $TSContext, eventType: 'PostPush' | 'PostPull target: 'es2020', script: 'jsx', renderTypeDeclarations: true, - }, + apiConfiguration, + } as AmplifyUIBuilder.ReactStartCodegenJobData, }, - autoGenerateForms: studioClient.metadata.autoGenerateForms && studioClient.isGraphQLSupported, + autoGenerateForms: studioClient.metadata.autoGenerateForms && studioClient.isGraphQLSupported && hasDataAPI, features: studioClient.metadata.formFeatureFlags, }; // SDK will throw if this is undefined diff --git a/packages/amplify-util-uibuilder/src/commands/utils/getApiConfiguration.ts b/packages/amplify-util-uibuilder/src/commands/utils/getApiConfiguration.ts new file mode 100644 index 00000000000..a9aae79d36d --- /dev/null +++ b/packages/amplify-util-uibuilder/src/commands/utils/getApiConfiguration.ts @@ -0,0 +1,73 @@ +import { printer } from '@aws-amplify/amplify-prompts'; +import { AmplifyStudioClient } from '../../clients'; +import { $TSContext } from '@aws-amplify/amplify-cli-core'; +import { getUiBuilderComponentsPath } from './getUiBuilderComponentsPath'; +import { getCodegenConfig } from 'amplify-codegen'; +import { ApiConfiguration } from 'aws-sdk/clients/amplifyuibuilder'; +import path from 'path'; + +//a posix formatted relative path must always be returned as the return values are used directly in jsx files as import paths +export function relativeToComponentsPath(importPath: string, context: $TSContext): string { + const componentsPath = getUiBuilderComponentsPath(context); + const segments = path.relative(componentsPath, importPath).split(path.sep); + return path.posix.join(...segments); +} + +export function getApiConfiguration(studioClient: AmplifyStudioClient, context: $TSContext): ApiConfiguration { + if (studioClient.isDataStoreEnabled) { + return { + dataStoreConfig: {}, + }; + } + + if (studioClient.metadata.isGraphQLEnabled) { + printer.debug('building graphql config'); + // attempt to get api codegen info + const projectPath = context.exeInfo.localEnvInfo.projectPath; + let promptForUpdateCodegen = false; + + try { + const codegenConfig = getCodegenConfig(projectPath); + const typesPath = codegenConfig.getGeneratedTypesPath(); + const apiConfiguration = { + graphQLConfig: { + typesFilePath: (typesPath && relativeToComponentsPath(typesPath, context)) || '', + queriesFilePath: relativeToComponentsPath(codegenConfig.getGeneratedQueriesPath(), context), + mutationsFilePath: relativeToComponentsPath(codegenConfig.getGeneratedMutationsPath(), context), + subscriptionsFilePath: relativeToComponentsPath(codegenConfig.getGeneratedSubscriptionsPath(), context), + fragmentsFilePath: relativeToComponentsPath(codegenConfig.getGeneratedFragmentsPath(), context), + }, + }; + + const minQueryDepth = 3; + const isQueryingTooShallow = (codegenConfig.getQueryMaxDepth() || 0) < minQueryDepth; + + if (studioClient.metadata.formFeatureFlags.isRelationshipSupported && isQueryingTooShallow) { + promptForUpdateCodegen = true; + printer.warn(`Forms with relationships require a maximum query depth of at least ${minQueryDepth}.`); + } + return apiConfiguration; + } catch { + promptForUpdateCodegen = true; + printer.warn( + 'Unable to successfully configure component generation for GraphQL. This will impact generating forms and components bound to your data models.', + ); + } finally { + if (promptForUpdateCodegen) { + printer.warn(`Run 'amplify update codegen' to ensure GraphQL configurations for your project are correct.`); + } + } + } + + return { + noApiConfig: {}, + }; +} + +export function hasDataStoreConfiguration(apiConfiguration: ApiConfiguration): boolean { + return apiConfiguration.dataStoreConfig !== undefined; +} + +export function hasGraphQLConfiguration(apiConfiguration: ApiConfiguration): boolean { + return apiConfiguration.graphQLConfig !== undefined; +} diff --git a/packages/amplify-util-uibuilder/types/amplify-codegen.d.ts b/packages/amplify-util-uibuilder/types/amplify-codegen.d.ts new file mode 100644 index 00000000000..9301b96ea77 --- /dev/null +++ b/packages/amplify-util-uibuilder/types/amplify-codegen.d.ts @@ -0,0 +1,12 @@ +declare module 'amplify-codegen' { + export function getCodegenConfig(projectPath: string | undefined): CodegenConfigHelper; + + export type CodegenConfigHelper = { + getGeneratedTypesPath: () => string | undefined; + getGeneratedQueriesPath: () => string; + getGeneratedMutationsPath: () => string; + getGeneratedSubscriptionsPath: () => string; + getGeneratedFragmentsPath: () => string; + getQueryMaxDepth: () => number | undefined; + }; +} diff --git a/yarn.lock b/yarn.lock index 2b76b007248..628e60edc8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -105,7 +105,7 @@ __metadata: "@types/node": ^12.12.6 "@types/ws": ^8.2.2 amplify-velocity-template: 1.4.12 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 chalk: ^4.1.1 cors: ^2.8.5 dataloader: ^2.0.0 @@ -213,7 +213,7 @@ __metadata: amplify-headless-interface: 1.17.4 amplify-util-headless-input: 1.9.14 aws-cdk-lib: ~2.80.0 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 axios: ^0.26.0 chalk: ^4.1.1 change-case: ^4.1.1 @@ -260,7 +260,7 @@ __metadata: "@aws-amplify/amplify-prompts": 2.8.1 "@types/folder-hash": ^4.0.1 archiver: ^5.3.0 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 chalk: ^4.1.1 cloudform-types: ^4.2.0 enquirer: ^2.3.6 @@ -289,7 +289,7 @@ __metadata: amplify-headless-interface: 1.17.4 amplify-util-headless-input: 1.9.14 aws-cdk-lib: ~2.80.0 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 constructs: ^10.0.5 fs-extra: ^8.1.0 lodash: ^4.17.21 @@ -333,7 +333,7 @@ __metadata: "@aws-amplify/amplify-environment-parameters": 1.7.4 "@aws-amplify/amplify-prompts": 2.8.1 "@aws-amplify/amplify-provider-awscloudformation": 8.4.0 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 chalk: ^4.1.1 fs-extra: ^8.1.0 lodash: ^4.17.21 @@ -350,7 +350,7 @@ __metadata: "@aws-amplify/amplify-cli-core": 4.2.4 "@aws-amplify/amplify-prompts": 2.8.1 "@aws-sdk/client-rekognition": ^3.303.0 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 chalk: ^4.1.1 fs-extra: ^8.1.0 uuid: ^8.3.2 @@ -369,7 +369,7 @@ __metadata: amplify-headless-interface: 1.17.4 amplify-util-headless-input: 1.9.14 aws-cdk-lib: ~2.80.0 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 chalk: ^4.1.1 cloudform-types: ^4.2.0 constructs: ^10.0.5 @@ -476,7 +476,7 @@ __metadata: "@aws-amplify/amplify-cli-core": 4.2.4 "@aws-amplify/amplify-e2e-core": 5.2.0 "@types/ini": ^1.3.30 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 dotenv: ^8.2.0 execa: ^5.1.1 fs-extra: ^8.1.0 @@ -527,7 +527,7 @@ __metadata: amplify-headless-interface: 1.17.4 aws-amplify: ^4.2.8 aws-appsync: ^4.1.1 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 chalk: ^4.1.1 dotenv: ^8.2.0 execa: ^5.1.1 @@ -554,7 +554,7 @@ __metadata: dependencies: "@aws-amplify/amplify-cli-core": 4.2.4 ajv: ^6.12.6 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 lodash: ^4.17.21 mkdirp: ^1.0.4 ts-json-schema-generator: ~1.1.2 @@ -757,7 +757,7 @@ __metadata: "@aws-amplify/amplify-prompts": 2.8.1 "@types/node": ^12.12.6 "@types/openpgp": ^4.4.18 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 detect-port: ^1.3.0 execa: ^5.1.1 fs-extra: ^8.1.0 @@ -805,7 +805,7 @@ __metadata: amplify-codegen: ^4.2.0 archiver: ^5.3.0 aws-cdk-lib: ~2.80.0 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 bottleneck: 2.19.5 chalk: ^4.1.1 cloudform-types: ^4.2.0 @@ -854,7 +854,7 @@ __metadata: resolution: "@aws-amplify/amplify-util-import@workspace:packages/amplify-util-import" dependencies: "@types/node": ^12.12.6 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 languageName: unknown linkType: soft @@ -895,7 +895,7 @@ __metadata: amplify-nodejs-function-runtime-provider: 2.5.4 amplify-storage-simulator: 1.10.0 aws-appsync: ^4.1.4 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 aws-sdk-mock: ^5.8.0 axios: ^0.26.0 chokidar: ^3.5.3 @@ -940,7 +940,7 @@ __metadata: "@types/semver": ^7.1.0 "@types/tiny-async-pool": ^2.0.0 amplify-codegen: ^4.2.0 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 fs-extra: ^8.1.0 node-fetch: ^2.6.7 ora: ^4.0.3 @@ -1113,7 +1113,7 @@ __metadata: amplify-nodejs-function-runtime-provider: 2.5.4 amplify-python-function-runtime-provider: 2.4.27 aws-cdk-lib: ~2.80.0 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 chalk: ^4.1.1 ci-info: ^3.8.0 cli-table3: ^0.6.0 @@ -13887,7 +13887,7 @@ __metadata: resolution: "amplify-dynamodb-simulator@workspace:packages/amplify-dynamodb-simulator" dependencies: "@aws-amplify/amplify-cli-core": 4.2.4 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 detect-port: ^1.3.0 execa: ^5.1.1 fs-extra: ^8.1.0 @@ -13928,7 +13928,7 @@ __metadata: aws-amplify: ^4.2.8 aws-appsync: ^4.1.1 aws-cdk-lib: ~2.80.0 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 axios: ^0.26.0 circleci-api: ^4.1.4 constructs: ^10.0.5 @@ -14090,7 +14090,7 @@ __metadata: "@types/serve-static": ^1.13.3 "@types/uuid": ^8.3.1 "@types/xml": ^1.0.4 - aws-sdk: ^2.1405.0 + aws-sdk: ^2.1426.0 body-parser: ^1.19.2 cors: ^2.8.5 etag: ^1.8.1 @@ -14866,9 +14866,9 @@ __metadata: languageName: node linkType: hard -"aws-sdk@npm:^2.1405.0": - version: 2.1405.0 - resolution: "aws-sdk@npm:2.1405.0" +"aws-sdk@npm:^2.1426.0": + version: 2.1426.0 + resolution: "aws-sdk@npm:2.1426.0" dependencies: buffer: 4.9.2 events: 1.1.1 @@ -14880,7 +14880,7 @@ __metadata: util: ^0.12.4 uuid: 8.0.0 xml2js: 0.5.0 - checksum: e7e06ba1d4b37cd10dae568bc17526c3d5d17532d37c1743f8589420ce9a4a489ff87cdb5ff53baa6138dbca79048d8b2f26d1a2fce8d2b11eb4eec6e9b12cb8 + checksum: 4d79be6a7ea7436989d8dc183f4a5a881a8935e8ab93270b6c1d5caac7f93e640b979c48a0af2569a9b063175ec3137427ba8a6545cada1f858b938d3cbab46b languageName: node linkType: hard