Skip to content

Commit aa61fe5

Browse files
committed
feat: generate components using graphql
1 parent d406c4d commit aa61fe5

File tree

7 files changed

+174
-27
lines changed

7 files changed

+174
-27
lines changed

packages/amplify-util-uibuilder/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"@aws-amplify/amplify-cli-core": "4.1.0",
1919
"@aws-amplify/amplify-prompts": "2.8.0",
2020
"@aws-amplify/codegen-ui": "2.14.2",
21-
"@aws-amplify/codegen-ui-react": "2.14.2",
21+
"@aws-amplify/codegen-ui-react": "2.14.3-graphql-support-0d1e240.0",
2222
"amplify-codegen": "^4.1.2",
2323
"aws-sdk": "^2.1354.0",
2424
"fs-extra": "^8.1.0",

packages/amplify-util-uibuilder/src/clients/amplify-studio-client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type StudioMetadata = {
1717
isRelationshipSupported: boolean;
1818
isNonModelSupported: boolean;
1919
};
20+
isGraphQLEnabled: boolean;
2021
};
2122

2223
/**
@@ -122,6 +123,7 @@ export default class AmplifyStudioClient {
122123
isRelationshipSupported: false,
123124
isNonModelSupported: false,
124125
},
126+
isGraphQLEnabled: false,
125127
};
126128
}
127129

@@ -145,6 +147,7 @@ export default class AmplifyStudioClient {
145147
isRelationshipSupported: response.features?.isRelationshipSupported === 'true',
146148
isNonModelSupported: response.features?.isNonModelSupported === 'true',
147149
},
150+
isGraphQLEnabled: true,
148151
};
149152
} catch (err) {
150153
throw new Error(`Failed to load metadata: ${err.message}`);

packages/amplify-util-uibuilder/src/commands/generateComponents.ts

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { StudioSchema } from '@aws-amplify/codegen-ui';
33
import ora from 'ora';
44
import { printer } from '@aws-amplify/amplify-prompts';
5+
import type { Form } from 'aws-sdk/clients/amplifyuibuilder';
56
import { $TSContext } from '@aws-amplify/amplify-cli-core';
67
import { AmplifyStudioClient } from '../clients';
78
import {
@@ -18,7 +19,11 @@ import {
1819
deleteDetachedForms,
1920
hasStorageField,
2021
isFormSchema,
22+
getUiBuilderComponentsPath,
2123
} from './utils';
24+
import { getCodegenConfig } from 'amplify-codegen';
25+
import { GraphqlRenderConfig, DataStoreRenderConfig } from '@aws-amplify/codegen-ui-react';
26+
import path from 'path';
2227

2328
/**
2429
* Pulls ui components from Studio backend and generates the code in the user's file system
@@ -37,24 +42,83 @@ export const run = async (context: $TSContext, eventType: 'PostPush' | 'PostPull
3742
studioClient.isGraphQLSupported ? getAmplifyDataSchema(context) : Promise.resolve(undefined),
3843
]);
3944

40-
const nothingWouldAutogenerate =
41-
!dataSchema || !studioClient.metadata.autoGenerateForms || !studioClient.isGraphQLSupported || !studioClient.isDataStoreEnabled;
45+
let canCodegenGraphqlComponents = false;
46+
let apiConfiguration: GraphqlRenderConfig | DataStoreRenderConfig = {
47+
dataApi: 'DataStore',
48+
};
49+
50+
if (!studioClient.isDataStoreEnabled && studioClient.metadata.isGraphQLEnabled) {
51+
// attempt to get api codegen info
52+
const projectPath = context.exeInfo.localEnvInfo.projectPath;
53+
printer.debug('building graphql config');
54+
55+
56+
try {
57+
const codegenConfig = getCodegenConfig(projectPath);
58+
printer.debug(`parsed types path: '${path.parse(codegenConfig.getGeneratedTypesPath())}'`);
59+
printer.debug(`components path: '${getUiBuilderComponentsPath(context)}'`);
60+
printer.debug(`split parts: '${codegenConfig.getGeneratedTypesPath().split(path.sep)}'`);
61+
printer.debug(`posix style? '${codegenConfig.getGeneratedTypesPath().split(path.sep).join('/')}'`);
62+
printer.debug(`relative? '${path.relative(getUiBuilderComponentsPath(context),codegenConfig.getGeneratedTypesPath().split(path.sep).join('/'))}'`);
63+
apiConfiguration = {
64+
dataApi: 'GraphQL',
65+
typesFilePath: codegenConfig.getGeneratedTypesPath(),
66+
queriesFilePath: codegenConfig.getGeneratedQueriesPath(),
67+
mutationsFilePath: codegenConfig.getGeneratedMutationsPath(),
68+
subscriptionsFilePath: codegenConfig.getGeneratedSubscriptionsPath(),
69+
fragmentsFilePath: codegenConfig.getGeneratedFragmentsPath(),
70+
};
71+
canCodegenGraphqlComponents = true;
72+
printer.debug(`graphql config built: ${JSON.stringify(apiConfiguration)}`);
73+
} catch {
74+
canCodegenGraphqlComponents = false;
75+
printer.debug('unable to build configuration');
76+
}
77+
}
78+
79+
const hasDataAPI = studioClient.isDataStoreEnabled || canCodegenGraphqlComponents;
80+
81+
const willAutogenerateItems = dataSchema && studioClient.metadata.autoGenerateForms && studioClient.isGraphQLSupported && hasDataAPI;
82+
83+
if (!hasDataAPI) {
84+
// filter components and forms that have data configurations and printer.warn()
85+
componentSchemas.entities.forEach((e) => {
86+
printer.debug(`component: ${JSON.stringify(e)}`);
87+
return false;
88+
});
89+
90+
const [formsToSkip, formsToGenerate] = formSchemas.entities.reduce(
91+
([toSkip, toGenerate], e) => {
92+
// form is configured for appsync API, cannot be generated
93+
if (e.dataType.dataSourceType === 'DataStore') {
94+
toSkip.push(e);
95+
} else {
96+
toGenerate.push(e);
97+
}
98+
return [toSkip, toGenerate];
99+
},
100+
[[], []] as [Form[], Form[]],
101+
);
102+
formSchemas.entities = formsToGenerate;
103+
printer.warn(`Skipping the following forms: ${formsToSkip.map(f => f.name).join(', ')}`);
104+
}
42105

43-
if (nothingWouldAutogenerate && [componentSchemas, themeSchemas, formSchemas].every((group) => !group.entities.length)) {
106+
if (!willAutogenerateItems && [componentSchemas, themeSchemas, formSchemas].every((group) => !group.entities.length)) {
44107
printer.debug('Skipping UI component generation since none are found.');
45108
return;
46109
}
47110
spinner.start('Generating UI components...');
48111

49112
const generatedResults = {
50-
component: generateUiBuilderComponents(context, componentSchemas.entities, dataSchema),
51-
theme: generateUiBuilderThemes(context, themeSchemas.entities),
113+
component: generateUiBuilderComponents(context, componentSchemas.entities, dataSchema, apiConfiguration),
114+
theme: generateUiBuilderThemes(context, themeSchemas.entities, apiConfiguration),
52115
form: generateUiBuilderForms(
53116
context,
54117
formSchemas.entities,
55118
dataSchema,
56-
studioClient.metadata.autoGenerateForms && studioClient.isGraphQLSupported && studioClient.isDataStoreEnabled,
119+
studioClient.metadata.autoGenerateForms && studioClient.isGraphQLSupported && hasDataAPI,
57120
studioClient.metadata.formFeatureFlags,
121+
apiConfiguration,
58122
),
59123
};
60124

@@ -98,9 +162,9 @@ export const run = async (context: $TSContext, eventType: 'PostPush' | 'PostPull
98162
});
99163
});
100164

101-
generateAmplifyUiBuilderIndexFile(context, successfulSchemas);
165+
generateAmplifyUiBuilderIndexFile(context, successfulSchemas, apiConfiguration);
102166

103-
generateAmplifyUiBuilderUtilFile(context, { hasForms: hasSuccessfulForm, hasViews: false });
167+
generateAmplifyUiBuilderUtilFile(context, { hasForms: hasSuccessfulForm, hasViews: false }, apiConfiguration);
104168

105169
if (failedResponseNames.length > 0) {
106170
spinner.fail(`Failed to sync the following components: ${failedResponseNames.join(', ')}`);

packages/amplify-util-uibuilder/src/commands/utils/codegenResources.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,42 @@ import {
2525
UtilTemplateType,
2626
ReactUtilsStudioTemplateRenderer,
2727
ReactThemeStudioTemplateRendererOptions,
28+
ReactRenderConfig,
29+
GraphqlRenderConfig,
30+
DataStoreRenderConfig,
2831
} from '@aws-amplify/codegen-ui-react';
2932
import { printer } from '@aws-amplify/amplify-prompts';
3033
import { $TSContext } from '@aws-amplify/amplify-cli-core';
3134
import { getUiBuilderComponentsPath } from './getUiBuilderComponentsPath';
3235
import { ModelIntrospectionSchema } from '@aws-amplify/appsync-modelgen-plugin';
3336

34-
const config = {
37+
const baseConfig: ReactRenderConfig = {
3538
module: ModuleKind.ES2020,
3639
target: ScriptTarget.ES2020,
3740
script: ScriptKind.JSX,
3841
renderTypeDeclarations: true,
42+
apiConfiguration: {
43+
dataApi: 'DataStore',
44+
},
3945
};
4046

4147
/**
4248
* Writes component file to the work space
4349
*/
44-
export const createUiBuilderComponent = (context: $TSContext, schema: StudioComponent, dataSchema?: GenericDataSchema): StudioComponent => {
50+
export const createUiBuilderComponent = (
51+
context: $TSContext,
52+
schema: StudioComponent,
53+
dataSchema?: GenericDataSchema,
54+
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
55+
): StudioComponent => {
4556
const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
57+
const config = {
58+
...baseConfig,
59+
apiConfiguration,
60+
};
61+
4662
const rendererFactory = new StudioTemplateRendererFactory(
47-
(component: StudioComponent) => new AmplifyRenderer(component as StudioComponent, config, dataSchema),
63+
(component: StudioComponent) => new AmplifyRenderer(component, config, dataSchema) as unknown as StudioTemplateRenderer<unknown, StudioComponent, FrameworkOutputManager<unknown>, RenderTextComponentResponse>,
4864
);
4965

5066
const outputPathDir = uiBuilderComponentsPath;
@@ -64,8 +80,13 @@ export const createUiBuilderTheme = (
6480
context: $TSContext,
6581
schema: StudioTheme,
6682
options?: ReactThemeStudioTemplateRendererOptions,
83+
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
6784
): StudioTheme => {
6885
const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
86+
const config = {
87+
...baseConfig,
88+
apiConfiguration,
89+
};
6990
const rendererFactory = new StudioTemplateRendererFactory(
7091
(component: StudioTheme) =>
7192
new ReactThemeStudioTemplateRenderer(component, config, options) as unknown as StudioTemplateRenderer<
@@ -101,8 +122,13 @@ export const createUiBuilderForm = (
101122
schema: StudioForm,
102123
dataSchema?: GenericDataSchema,
103124
formFeatureFlags?: FormFeatureFlags,
125+
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
104126
): StudioForm => {
105127
const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
128+
const config = {
129+
...baseConfig,
130+
apiConfiguration,
131+
};
106132
const rendererFactory = new StudioTemplateRendererFactory(
107133
(form: StudioForm) =>
108134
new AmplifyFormRenderer(form, dataSchema, config, formFeatureFlags) as unknown as StudioTemplateRenderer<
@@ -133,8 +159,16 @@ export const createUiBuilderForm = (
133159
/**
134160
* Writes index file to the work space
135161
*/
136-
export const generateAmplifyUiBuilderIndexFile = (context: $TSContext, schemas: StudioSchema[]): void => {
162+
export const generateAmplifyUiBuilderIndexFile = (
163+
context: $TSContext,
164+
schemas: StudioSchema[],
165+
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
166+
): void => {
137167
const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
168+
const config = {
169+
...baseConfig,
170+
apiConfiguration,
171+
};
138172
const rendererFactory = new StudioTemplateRendererFactory(
139173
(schema: StudioSchema[]) =>
140174
new ReactIndexStudioTemplateRenderer(schema, config) as unknown as StudioTemplateRenderer<
@@ -170,10 +204,24 @@ type UtilFileChecks = {
170204
/**
171205
* Writes utils file to the work space
172206
*/
173-
export const generateAmplifyUiBuilderUtilFile = (context: $TSContext, { hasForms, hasViews }: UtilFileChecks): void => {
207+
export const generateAmplifyUiBuilderUtilFile = (
208+
context: $TSContext,
209+
{ hasForms, hasViews }: UtilFileChecks,
210+
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
211+
): void => {
174212
const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
213+
const config = {
214+
...baseConfig,
215+
apiConfiguration,
216+
};
175217
const rendererFactory = new StudioTemplateRendererFactory(
176-
(utils: UtilTemplateType[]) => new ReactUtilsStudioTemplateRenderer(utils, config),
218+
(utils: UtilTemplateType[]) =>
219+
new ReactUtilsStudioTemplateRenderer(utils, config) as unknown as StudioTemplateRenderer<
220+
unknown,
221+
UtilTemplateType[],
222+
FrameworkOutputManager<unknown>,
223+
RenderTextComponentResponse
224+
>,
177225
);
178226

179227
const outputPathDir = uiBuilderComponentsPath;

packages/amplify-util-uibuilder/src/commands/utils/syncAmplifyUiBuilderComponents.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '@aws-amplify/codegen-ui';
1212
import { createUiBuilderComponent, createUiBuilderForm, createUiBuilderTheme, generateBaseForms } from './codegenResources';
1313
import { getUiBuilderComponentsPath } from './getUiBuilderComponentsPath';
14+
import { DataStoreRenderConfig, GraphqlRenderConfig } from '@aws-amplify/codegen-ui-react';
1415

1516
type CodegenResponse<T extends StudioSchema> =
1617
| {
@@ -34,10 +35,11 @@ export const generateUiBuilderComponents = (
3435
context: $TSContext,
3536
componentSchemas: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
3637
dataSchema?: GenericDataSchema,
38+
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
3739
): CodegenResponse<StudioComponent>[] => {
3840
const componentResults = componentSchemas.map<CodegenResponse<StudioComponent>>((schema) => {
3941
try {
40-
const component = createUiBuilderComponent(context, schema, dataSchema);
42+
const component = createUiBuilderComponent(context, schema, dataSchema, apiConfiguration);
4143
return { resultType: 'SUCCESS', schema: component };
4244
} catch (e) {
4345
printer.debug(`Failure caught processing ${schema.name}`);
@@ -58,13 +60,17 @@ export const generateUiBuilderComponents = (
5860
* Returns instances of StudioTheme from theme schemas
5961
*/
6062
// eslint-disable-next-line @typescript-eslint/no-explicit-any
61-
export const generateUiBuilderThemes = (context: $TSContext, themeSchemas: any[]): CodegenResponse<StudioTheme>[] => {
63+
export const generateUiBuilderThemes = (
64+
context: $TSContext,
65+
themeSchemas: any[],
66+
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
67+
): CodegenResponse<StudioTheme>[] => {
6268
if (themeSchemas.length === 0) {
6369
return [generateDefaultTheme(context)];
6470
}
6571
const themeResults = themeSchemas.map<CodegenResponse<StudioTheme>>((schema) => {
6672
try {
67-
const theme = createUiBuilderTheme(context, schema);
73+
const theme = createUiBuilderTheme(context, schema, undefined, apiConfiguration);
6874
return { resultType: 'SUCCESS', schema: theme };
6975
} catch (e) {
7076
printer.debug(`Failure caught processing ${schema.name}`);
@@ -82,9 +88,12 @@ export const generateUiBuilderThemes = (context: $TSContext, themeSchemas: any[]
8288
/**
8389
* Generates the defaultTheme in the user's project that's exported from @aws-amplify/codegen-ui-react
8490
*/
85-
const generateDefaultTheme = (context: $TSContext): CodegenResponse<StudioTheme> => {
91+
const generateDefaultTheme = (
92+
context: $TSContext,
93+
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
94+
): CodegenResponse<StudioTheme> => {
8695
try {
87-
const theme = createUiBuilderTheme(context, { name: 'studioTheme', values: [] }, { renderDefaultTheme: true });
96+
const theme = createUiBuilderTheme(context, { name: 'studioTheme', values: [] }, { renderDefaultTheme: true }, apiConfiguration);
8897
printer.debug(`Generated default theme in ${getUiBuilderComponentsPath(context)}`);
8998
return { resultType: 'SUCCESS', schema: theme };
9099
} catch (e) {
@@ -104,6 +113,7 @@ export const generateUiBuilderForms = (
104113
dataSchema?: GenericDataSchema,
105114
autoGenerateForms?: boolean,
106115
formFeatureFlags?: FormFeatureFlags,
116+
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
107117
): CodegenResponse<StudioForm>[] => {
108118
const modelMap: { [model: string]: Set<'create' | 'update'> } = {};
109119
if (dataSchema?.dataSourceType === 'DataStore' && autoGenerateForms) {
@@ -115,7 +125,7 @@ export const generateUiBuilderForms = (
115125
}
116126
const codegenForm = (schema: StudioForm): CodegenResponse<StudioForm> => {
117127
try {
118-
const form = createUiBuilderForm(context, schema, dataSchema, formFeatureFlags);
128+
const form = createUiBuilderForm(context, schema, dataSchema, formFeatureFlags, apiConfiguration);
119129
return { resultType: 'SUCCESS', schema: form };
120130
} catch (e) {
121131
printer.debug(`Failure caught processing ${schema.name}`);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
declare module 'amplify-codegen' {
2+
export function getCodegenConfig(projectPath: string | undefined): CodegenConfigHelper;
3+
4+
export type CodegenConfigHelper = {
5+
getGeneratedTypesPath: () => string;
6+
getGeneratedQueriesPath: () => string;
7+
getGeneratedMutationsPath: () => string;
8+
getGeneratedSubscriptionsPath: () => string;
9+
getGeneratedFragmentsPath: () => string;
10+
};
11+
}

0 commit comments

Comments
 (0)