Skip to content

Commit

Permalink
feat: generate components using graphql
Browse files Browse the repository at this point in the history
  • Loading branch information
awinberg-aws committed Jul 6, 2023
1 parent d406c4d commit aa61fe5
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 27 deletions.
2 changes: 1 addition & 1 deletion packages/amplify-util-uibuilder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@aws-amplify/amplify-cli-core": "4.1.0",
"@aws-amplify/amplify-prompts": "2.8.0",
"@aws-amplify/codegen-ui": "2.14.2",
"@aws-amplify/codegen-ui-react": "2.14.2",
"@aws-amplify/codegen-ui-react": "2.14.3-graphql-support-0d1e240.0",
"amplify-codegen": "^4.1.2",
"aws-sdk": "^2.1354.0",
"fs-extra": "^8.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type StudioMetadata = {
isRelationshipSupported: boolean;
isNonModelSupported: boolean;
};
isGraphQLEnabled: boolean;
};

/**
Expand Down Expand Up @@ -122,6 +123,7 @@ export default class AmplifyStudioClient {
isRelationshipSupported: false,
isNonModelSupported: false,
},
isGraphQLEnabled: false,
};
}

Expand All @@ -145,6 +147,7 @@ export default class AmplifyStudioClient {
isRelationshipSupported: response.features?.isRelationshipSupported === 'true',
isNonModelSupported: response.features?.isNonModelSupported === 'true',
},
isGraphQLEnabled: true,
};
} catch (err) {
throw new Error(`Failed to load metadata: ${err.message}`);
Expand Down
80 changes: 72 additions & 8 deletions packages/amplify-util-uibuilder/src/commands/generateComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { StudioSchema } from '@aws-amplify/codegen-ui';
import ora from 'ora';
import { printer } from '@aws-amplify/amplify-prompts';
import type { Form } from 'aws-sdk/clients/amplifyuibuilder';
import { $TSContext } from '@aws-amplify/amplify-cli-core';
import { AmplifyStudioClient } from '../clients';
import {
Expand All @@ -18,7 +19,11 @@ import {
deleteDetachedForms,
hasStorageField,
isFormSchema,
getUiBuilderComponentsPath,
} from './utils';
import { getCodegenConfig } from 'amplify-codegen';
import { GraphqlRenderConfig, DataStoreRenderConfig } from '@aws-amplify/codegen-ui-react';
import path from 'path';

/**
* Pulls ui components from Studio backend and generates the code in the user's file system
Expand All @@ -37,24 +42,83 @@ 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;
let canCodegenGraphqlComponents = false;
let apiConfiguration: GraphqlRenderConfig | DataStoreRenderConfig = {
dataApi: 'DataStore',
};

if (!studioClient.isDataStoreEnabled && studioClient.metadata.isGraphQLEnabled) {
// attempt to get api codegen info
const projectPath = context.exeInfo.localEnvInfo.projectPath;
printer.debug('building graphql config');


try {
const codegenConfig = getCodegenConfig(projectPath);
printer.debug(`parsed types path: '${path.parse(codegenConfig.getGeneratedTypesPath())}'`);
printer.debug(`components path: '${getUiBuilderComponentsPath(context)}'`);
printer.debug(`split parts: '${codegenConfig.getGeneratedTypesPath().split(path.sep)}'`);
printer.debug(`posix style? '${codegenConfig.getGeneratedTypesPath().split(path.sep).join('/')}'`);
printer.debug(`relative? '${path.relative(getUiBuilderComponentsPath(context),codegenConfig.getGeneratedTypesPath().split(path.sep).join('/'))}'`);
apiConfiguration = {
dataApi: 'GraphQL',
typesFilePath: codegenConfig.getGeneratedTypesPath(),
queriesFilePath: codegenConfig.getGeneratedQueriesPath(),
mutationsFilePath: codegenConfig.getGeneratedMutationsPath(),
subscriptionsFilePath: codegenConfig.getGeneratedSubscriptionsPath(),
fragmentsFilePath: codegenConfig.getGeneratedFragmentsPath(),
};
canCodegenGraphqlComponents = true;
printer.debug(`graphql config built: ${JSON.stringify(apiConfiguration)}`);
} catch {
canCodegenGraphqlComponents = false;
printer.debug('unable to build configuration');
}
}

const hasDataAPI = studioClient.isDataStoreEnabled || canCodegenGraphqlComponents;

const willAutogenerateItems = dataSchema && studioClient.metadata.autoGenerateForms && studioClient.isGraphQLSupported && hasDataAPI;

if (!hasDataAPI) {
// filter components and forms that have data configurations and printer.warn()
componentSchemas.entities.forEach((e) => {
printer.debug(`component: ${JSON.stringify(e)}`);
return false;
});

const [formsToSkip, formsToGenerate] = formSchemas.entities.reduce(
([toSkip, toGenerate], e) => {
// form is configured for appsync API, cannot be generated
if (e.dataType.dataSourceType === 'DataStore') {
toSkip.push(e);
} else {
toGenerate.push(e);
}
return [toSkip, toGenerate];
},
[[], []] as [Form[], Form[]],
);
formSchemas.entities = formsToGenerate;
printer.warn(`Skipping the following forms: ${formsToSkip.map(f => f.name).join(', ')}`);
}

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

const generatedResults = {
component: generateUiBuilderComponents(context, componentSchemas.entities, dataSchema),
theme: generateUiBuilderThemes(context, themeSchemas.entities),
component: generateUiBuilderComponents(context, componentSchemas.entities, dataSchema, apiConfiguration),
theme: generateUiBuilderThemes(context, themeSchemas.entities, apiConfiguration),
form: generateUiBuilderForms(
context,
formSchemas.entities,
dataSchema,
studioClient.metadata.autoGenerateForms && studioClient.isGraphQLSupported && studioClient.isDataStoreEnabled,
studioClient.metadata.autoGenerateForms && studioClient.isGraphQLSupported && hasDataAPI,
studioClient.metadata.formFeatureFlags,
apiConfiguration,
),
};

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

generateAmplifyUiBuilderIndexFile(context, successfulSchemas);
generateAmplifyUiBuilderIndexFile(context, successfulSchemas, apiConfiguration);

generateAmplifyUiBuilderUtilFile(context, { hasForms: hasSuccessfulForm, hasViews: false });
generateAmplifyUiBuilderUtilFile(context, { hasForms: hasSuccessfulForm, hasViews: false }, apiConfiguration);

if (failedResponseNames.length > 0) {
spinner.fail(`Failed to sync the following components: ${failedResponseNames.join(', ')}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,42 @@ import {
UtilTemplateType,
ReactUtilsStudioTemplateRenderer,
ReactThemeStudioTemplateRendererOptions,
ReactRenderConfig,
GraphqlRenderConfig,
DataStoreRenderConfig,
} from '@aws-amplify/codegen-ui-react';
import { printer } from '@aws-amplify/amplify-prompts';
import { $TSContext } from '@aws-amplify/amplify-cli-core';
import { getUiBuilderComponentsPath } from './getUiBuilderComponentsPath';
import { ModelIntrospectionSchema } from '@aws-amplify/appsync-modelgen-plugin';

const config = {
const baseConfig: ReactRenderConfig = {
module: ModuleKind.ES2020,
target: ScriptTarget.ES2020,
script: ScriptKind.JSX,
renderTypeDeclarations: true,
apiConfiguration: {
dataApi: 'DataStore',
},
};

/**
* Writes component file to the work space
*/
export const createUiBuilderComponent = (context: $TSContext, schema: StudioComponent, dataSchema?: GenericDataSchema): StudioComponent => {
export const createUiBuilderComponent = (
context: $TSContext,
schema: StudioComponent,
dataSchema?: GenericDataSchema,
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
): StudioComponent => {
const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
const config = {
...baseConfig,
apiConfiguration,
};

const rendererFactory = new StudioTemplateRendererFactory(
(component: StudioComponent) => new AmplifyRenderer(component as StudioComponent, config, dataSchema),
(component: StudioComponent) => new AmplifyRenderer(component, config, dataSchema) as unknown as StudioTemplateRenderer<unknown, StudioComponent, FrameworkOutputManager<unknown>, RenderTextComponentResponse>,
);

const outputPathDir = uiBuilderComponentsPath;
Expand All @@ -64,8 +80,13 @@ export const createUiBuilderTheme = (
context: $TSContext,
schema: StudioTheme,
options?: ReactThemeStudioTemplateRendererOptions,
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
): StudioTheme => {
const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
const config = {
...baseConfig,
apiConfiguration,
};
const rendererFactory = new StudioTemplateRendererFactory(
(component: StudioTheme) =>
new ReactThemeStudioTemplateRenderer(component, config, options) as unknown as StudioTemplateRenderer<
Expand Down Expand Up @@ -101,8 +122,13 @@ export const createUiBuilderForm = (
schema: StudioForm,
dataSchema?: GenericDataSchema,
formFeatureFlags?: FormFeatureFlags,
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
): StudioForm => {
const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
const config = {
...baseConfig,
apiConfiguration,
};
const rendererFactory = new StudioTemplateRendererFactory(
(form: StudioForm) =>
new AmplifyFormRenderer(form, dataSchema, config, formFeatureFlags) as unknown as StudioTemplateRenderer<
Expand Down Expand Up @@ -133,8 +159,16 @@ export const createUiBuilderForm = (
/**
* Writes index file to the work space
*/
export const generateAmplifyUiBuilderIndexFile = (context: $TSContext, schemas: StudioSchema[]): void => {
export const generateAmplifyUiBuilderIndexFile = (
context: $TSContext,
schemas: StudioSchema[],
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
): void => {
const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
const config = {
...baseConfig,
apiConfiguration,
};
const rendererFactory = new StudioTemplateRendererFactory(
(schema: StudioSchema[]) =>
new ReactIndexStudioTemplateRenderer(schema, config) as unknown as StudioTemplateRenderer<
Expand Down Expand Up @@ -170,10 +204,24 @@ type UtilFileChecks = {
/**
* Writes utils file to the work space
*/
export const generateAmplifyUiBuilderUtilFile = (context: $TSContext, { hasForms, hasViews }: UtilFileChecks): void => {
export const generateAmplifyUiBuilderUtilFile = (
context: $TSContext,
{ hasForms, hasViews }: UtilFileChecks,
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
): void => {
const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
const config = {
...baseConfig,
apiConfiguration,
};
const rendererFactory = new StudioTemplateRendererFactory(
(utils: UtilTemplateType[]) => new ReactUtilsStudioTemplateRenderer(utils, config),
(utils: UtilTemplateType[]) =>
new ReactUtilsStudioTemplateRenderer(utils, config) as unknown as StudioTemplateRenderer<
unknown,
UtilTemplateType[],
FrameworkOutputManager<unknown>,
RenderTextComponentResponse
>,
);

const outputPathDir = uiBuilderComponentsPath;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@aws-amplify/codegen-ui';
import { createUiBuilderComponent, createUiBuilderForm, createUiBuilderTheme, generateBaseForms } from './codegenResources';
import { getUiBuilderComponentsPath } from './getUiBuilderComponentsPath';
import { DataStoreRenderConfig, GraphqlRenderConfig } from '@aws-amplify/codegen-ui-react';

type CodegenResponse<T extends StudioSchema> =
| {
Expand All @@ -34,10 +35,11 @@ export const generateUiBuilderComponents = (
context: $TSContext,
componentSchemas: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
dataSchema?: GenericDataSchema,
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
): CodegenResponse<StudioComponent>[] => {
const componentResults = componentSchemas.map<CodegenResponse<StudioComponent>>((schema) => {
try {
const component = createUiBuilderComponent(context, schema, dataSchema);
const component = createUiBuilderComponent(context, schema, dataSchema, apiConfiguration);
return { resultType: 'SUCCESS', schema: component };
} catch (e) {
printer.debug(`Failure caught processing ${schema.name}`);
Expand All @@ -58,13 +60,17 @@ export const generateUiBuilderComponents = (
* Returns instances of StudioTheme from theme schemas
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const generateUiBuilderThemes = (context: $TSContext, themeSchemas: any[]): CodegenResponse<StudioTheme>[] => {
export const generateUiBuilderThemes = (
context: $TSContext,
themeSchemas: any[],
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
): CodegenResponse<StudioTheme>[] => {
if (themeSchemas.length === 0) {
return [generateDefaultTheme(context)];
}
const themeResults = themeSchemas.map<CodegenResponse<StudioTheme>>((schema) => {
try {
const theme = createUiBuilderTheme(context, schema);
const theme = createUiBuilderTheme(context, schema, undefined, apiConfiguration);
return { resultType: 'SUCCESS', schema: theme };
} catch (e) {
printer.debug(`Failure caught processing ${schema.name}`);
Expand All @@ -82,9 +88,12 @@ export const generateUiBuilderThemes = (context: $TSContext, themeSchemas: any[]
/**
* Generates the defaultTheme in the user's project that's exported from @aws-amplify/codegen-ui-react
*/
const generateDefaultTheme = (context: $TSContext): CodegenResponse<StudioTheme> => {
const generateDefaultTheme = (
context: $TSContext,
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
): CodegenResponse<StudioTheme> => {
try {
const theme = createUiBuilderTheme(context, { name: 'studioTheme', values: [] }, { renderDefaultTheme: true });
const theme = createUiBuilderTheme(context, { name: 'studioTheme', values: [] }, { renderDefaultTheme: true }, apiConfiguration);
printer.debug(`Generated default theme in ${getUiBuilderComponentsPath(context)}`);
return { resultType: 'SUCCESS', schema: theme };
} catch (e) {
Expand All @@ -104,6 +113,7 @@ export const generateUiBuilderForms = (
dataSchema?: GenericDataSchema,
autoGenerateForms?: boolean,
formFeatureFlags?: FormFeatureFlags,
apiConfiguration?: GraphqlRenderConfig | DataStoreRenderConfig,
): CodegenResponse<StudioForm>[] => {
const modelMap: { [model: string]: Set<'create' | 'update'> } = {};
if (dataSchema?.dataSourceType === 'DataStore' && autoGenerateForms) {
Expand All @@ -115,7 +125,7 @@ export const generateUiBuilderForms = (
}
const codegenForm = (schema: StudioForm): CodegenResponse<StudioForm> => {
try {
const form = createUiBuilderForm(context, schema, dataSchema, formFeatureFlags);
const form = createUiBuilderForm(context, schema, dataSchema, formFeatureFlags, apiConfiguration);
return { resultType: 'SUCCESS', schema: form };
} catch (e) {
printer.debug(`Failure caught processing ${schema.name}`);
Expand Down
11 changes: 11 additions & 0 deletions packages/amplify-util-uibuilder/types/amplify-codegen.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
declare module 'amplify-codegen' {
export function getCodegenConfig(projectPath: string | undefined): CodegenConfigHelper;

export type CodegenConfigHelper = {
getGeneratedTypesPath: () => string;
getGeneratedQueriesPath: () => string;
getGeneratedMutationsPath: () => string;
getGeneratedSubscriptionsPath: () => string;
getGeneratedFragmentsPath: () => string;
};
}
Loading

0 comments on commit aa61fe5

Please sign in to comment.