@aws-amplify/amplify-util-uibuilder 1.9.0-replace-lambda-callouts-0.0 → 1.9.1-rc.06e15aa08a.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +48 -9
  2. package/lib/clients/amplify-studio-client.d.ts +5 -1
  3. package/lib/clients/amplify-studio-client.d.ts.map +1 -1
  4. package/lib/clients/amplify-studio-client.js +54 -8
  5. package/lib/clients/amplify-studio-client.js.map +1 -1
  6. package/lib/commands/generateComponents.d.ts.map +1 -1
  7. package/lib/commands/generateComponents.js +33 -34
  8. package/lib/commands/generateComponents.js.map +1 -1
  9. package/lib/commands/utils/codegenResources.d.ts +1 -15
  10. package/lib/commands/utils/codegenResources.d.ts.map +1 -1
  11. package/lib/commands/utils/codegenResources.js +1 -122
  12. package/lib/commands/utils/codegenResources.js.map +1 -1
  13. package/lib/commands/utils/detachedFormCleanup.d.ts +3 -4
  14. package/lib/commands/utils/detachedFormCleanup.d.ts.map +1 -1
  15. package/lib/commands/utils/detachedFormCleanup.js.map +1 -1
  16. package/lib/commands/utils/storageManager.d.ts +3 -4
  17. package/lib/commands/utils/storageManager.d.ts.map +1 -1
  18. package/lib/commands/utils/storageManager.js +1 -5
  19. package/lib/commands/utils/storageManager.js.map +1 -1
  20. package/lib/commands/utils/syncAmplifyUiBuilderComponents.d.ts +8 -16
  21. package/lib/commands/utils/syncAmplifyUiBuilderComponents.d.ts.map +1 -1
  22. package/lib/commands/utils/syncAmplifyUiBuilderComponents.js +186 -74
  23. package/lib/commands/utils/syncAmplifyUiBuilderComponents.js.map +1 -1
  24. package/package.json +28 -19
  25. package/src/__tests__/__snapshots__/syncAmplifyUiComponents.test.ts.snap +46 -0
  26. package/src/__tests__/cloneComponentsFromEnv.test.ts +8 -0
  27. package/src/__tests__/detachedFormCleanup.test.ts +7 -5
  28. package/src/__tests__/generateComponents.test.ts +187 -15
  29. package/src/__tests__/getAmplifyDataSchema.test.ts +1 -2
  30. package/src/__tests__/prePushHandler.test.ts +7 -0
  31. package/src/__tests__/storageManager.test.ts +7 -4
  32. package/src/__tests__/syncAmplifyUiComponents.test.ts +106 -95
  33. package/src/clients/amplify-studio-client.ts +56 -9
  34. package/src/commands/generateComponents.ts +45 -56
  35. package/src/commands/utils/codegenResources.ts +1 -225
  36. package/src/commands/utils/detachedFormCleanup.ts +3 -4
  37. package/src/commands/utils/storageManager.ts +3 -7
  38. package/src/commands/utils/syncAmplifyUiBuilderComponents.ts +210 -123
  39. package/tsconfig.tsbuildinfo +1 -1
  40. package/src/__tests__/createUiBuilderComponent.test.ts +0 -90
@@ -1,145 +1,232 @@
1
1
  import { printer } from '@aws-amplify/amplify-prompts';
2
- import { $TSContext } from '@aws-amplify/amplify-cli-core';
2
+ import type { GenericDataSchema, GenericDataField, GenericDataRelationshipType } from '@aws-amplify/codegen-ui';
3
3
  import {
4
- StudioComponent,
5
- StudioTheme,
6
- GenericDataSchema,
7
- StudioForm,
8
- StudioSchema,
9
- checkIsSupportedAsForm,
10
- FormFeatureFlags,
11
- } from '@aws-amplify/codegen-ui';
12
- import { createUiBuilderComponent, createUiBuilderForm, createUiBuilderTheme, generateBaseForms } from './codegenResources';
13
- import { getUiBuilderComponentsPath } from './getUiBuilderComponentsPath';
14
-
15
- type CodegenResponse<T extends StudioSchema> =
16
- | {
17
- resultType: 'SUCCESS';
18
- schema: T;
19
- schemaName?: string;
4
+ CodegenGenericDataEnum,
5
+ CodegenGenericDataFieldDataType,
6
+ CodegenGenericDataFields,
7
+ CodegenGenericDataModel,
8
+ CodegenGenericDataNonModel,
9
+ CodegenGenericDataRelationshipType,
10
+ CodegenJob,
11
+ CodegenJobGenericDataSchema,
12
+ } from 'aws-sdk/clients/amplifyuibuilder';
13
+ import fetch from 'node-fetch';
14
+ import { performance } from 'perf_hooks';
15
+ import path from 'path';
16
+ import fs from 'fs';
17
+ import asyncPool from 'tiny-async-pool';
18
+
19
+ const mapRelationshipTypeToCodegen = (
20
+ relationship: GenericDataRelationshipType | undefined,
21
+ ): CodegenGenericDataRelationshipType | undefined => {
22
+ if (!relationship) return undefined;
23
+
24
+ switch (relationship.type) {
25
+ case 'HAS_MANY':
26
+ return {
27
+ type: 'HAS_MANY',
28
+ relatedModelFields: relationship.relatedModelFields,
29
+ canUnlinkAssociatedModel: !!relationship.canUnlinkAssociatedModel,
30
+ relatedJoinFieldName: relationship.relatedJoinFieldName,
31
+ relatedJoinTableName: relationship.relatedJoinTableName,
32
+ belongsToFieldOnRelatedModel: relationship.belongsToFieldOnRelatedModel,
33
+ relatedModelName: relationship.relatedModelName,
34
+ };
35
+ case 'HAS_ONE':
36
+ return {
37
+ type: 'HAS_ONE',
38
+ associatedFields: relationship.associatedFields,
39
+ isHasManyIndex: !!relationship.isHasManyIndex,
40
+ relatedModelName: relationship.relatedModelName,
41
+ };
42
+ case 'BELONGS_TO':
43
+ return {
44
+ type: 'BELONGS_TO',
45
+ associatedFields: relationship.associatedFields,
46
+ isHasManyIndex: !!relationship.isHasManyIndex,
47
+ relatedModelName: relationship.relatedModelName,
48
+ };
49
+ default:
50
+ throw new Error('Invalid relationship type');
51
+ }
52
+ };
53
+
54
+ const mapDataFieldsToCodegen = (fields: { [fieldName: string]: GenericDataField }): CodegenGenericDataFields => {
55
+ const codegenFields: CodegenGenericDataFields = {};
56
+
57
+ Object.entries(fields).forEach(([fieldName, dataField]) => {
58
+ let dataType: CodegenGenericDataFieldDataType = '';
59
+ let dataTypeValue = '';
60
+ if (typeof dataField.dataType === 'object' && dataField.dataType !== null) {
61
+ if ('enum' in dataField.dataType) {
62
+ dataType = 'Enum';
63
+ dataTypeValue = dataField.dataType.enum;
64
+ } else if ('model' in dataField.dataType) {
65
+ dataType = 'Model';
66
+ dataTypeValue = dataField.dataType.model;
67
+ } else if ('nonModel' in dataField.dataType) {
68
+ dataType = 'NonModel';
69
+ dataTypeValue = dataField.dataType.nonModel;
70
+ }
71
+ } else {
72
+ dataType = dataField.dataType;
73
+ dataTypeValue = dataField.dataType;
20
74
  }
21
- | {
22
- resultType: 'FAILURE';
23
- schemaName: string;
24
- error: Error;
25
- schema?: T;
75
+ codegenFields[fieldName] = {
76
+ dataType: dataType,
77
+ dataTypeValue: dataTypeValue,
78
+ required: dataField.required,
79
+ readOnly: dataField.readOnly,
80
+ isArray: dataField.isArray,
26
81
  };
27
-
28
- // TODO: when types are updated in codegen-ui reference mappers here so the arguments accepted are not any
29
-
30
- /**
31
- * Returns instances of StudioComponent from the component schemas
32
- */
33
- export const generateUiBuilderComponents = (
34
- context: $TSContext,
35
- componentSchemas: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
36
- dataSchema?: GenericDataSchema,
37
- ): CodegenResponse<StudioComponent>[] => {
38
- const componentResults = componentSchemas.map<CodegenResponse<StudioComponent>>((schema) => {
39
- try {
40
- const component = createUiBuilderComponent(context, schema, dataSchema);
41
- return { resultType: 'SUCCESS', schema: component };
42
- } catch (e) {
43
- printer.debug(`Failure caught processing ${schema.name}`);
44
- printer.debug(e);
45
- return { resultType: 'FAILURE', schemaName: schema.name, error: e };
82
+ if (dataField.relationship) {
83
+ codegenFields[fieldName].relationship = mapRelationshipTypeToCodegen(dataField.relationship);
46
84
  }
47
85
  });
48
86
 
49
- printer.debug(
50
- `Generated ${componentResults.filter((result) => result.resultType === 'SUCCESS').length} components in ${getUiBuilderComponentsPath(
51
- context,
52
- )}`,
53
- );
54
- return componentResults;
87
+ return codegenFields;
55
88
  };
56
89
 
57
- /**
58
- * Returns instances of StudioTheme from theme schemas
59
- */
60
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
- export const generateUiBuilderThemes = (context: $TSContext, themeSchemas: any[]): CodegenResponse<StudioTheme>[] => {
62
- if (themeSchemas.length === 0) {
63
- return [generateDefaultTheme(context)];
64
- }
65
- const themeResults = themeSchemas.map<CodegenResponse<StudioTheme>>((schema) => {
66
- try {
67
- const theme = createUiBuilderTheme(context, schema);
68
- return { resultType: 'SUCCESS', schema: theme };
69
- } catch (e) {
70
- printer.debug(`Failure caught processing ${schema.name}`);
71
- printer.debug(e);
72
- return { resultType: 'FAILURE', schemaName: schema.name, error: e };
73
- }
90
+ export const mapGenericDataSchemaToCodegen = (genericDataSchema: GenericDataSchema): CodegenJobGenericDataSchema => {
91
+ const { models, nonModels, enums, dataSourceType } = genericDataSchema;
92
+ const codegenModels: { [key: string]: CodegenGenericDataModel } = {};
93
+ const codegenNonModels: { [key: string]: CodegenGenericDataNonModel } = {};
94
+ const codegenEnums: { [key: string]: CodegenGenericDataEnum } = {};
95
+
96
+ Object.entries(models).forEach(([modelName, genericDataModel]) => {
97
+ const modelFields = mapDataFieldsToCodegen(genericDataModel.fields);
98
+
99
+ codegenModels[modelName] = {
100
+ isJoinTable: genericDataModel.isJoinTable,
101
+ primaryKeys: genericDataModel.primaryKeys,
102
+ fields: modelFields,
103
+ };
74
104
  });
75
105
 
76
- printer.debug(
77
- `Generated ${themeResults.filter((result) => result.resultType === 'SUCCESS').length} themes in ${getUiBuilderComponentsPath(context)}`,
78
- );
79
- return themeResults;
106
+ Object.entries(nonModels).forEach(([nonModelName, genericDataModel]) => {
107
+ const nonModelFields = mapDataFieldsToCodegen(genericDataModel.fields);
108
+
109
+ codegenNonModels[nonModelName] = {
110
+ fields: nonModelFields,
111
+ };
112
+ });
113
+
114
+ Object.entries(enums).forEach(([enumName, genericEnum]) => {
115
+ codegenEnums[enumName] = {
116
+ values: genericEnum.values,
117
+ };
118
+ });
119
+
120
+ return {
121
+ models: codegenModels,
122
+ nonModels: codegenNonModels,
123
+ enums: codegenEnums,
124
+ dataSourceType,
125
+ };
80
126
  };
81
127
 
82
- /**
83
- * Generates the defaultTheme in the user's project that's exported from @aws-amplify/codegen-ui-react
84
- */
85
- const generateDefaultTheme = (context: $TSContext): CodegenResponse<StudioTheme> => {
86
- try {
87
- const theme = createUiBuilderTheme(context, { name: 'studioTheme', values: [] }, { renderDefaultTheme: true });
88
- printer.debug(`Generated default theme in ${getUiBuilderComponentsPath(context)}`);
89
- return { resultType: 'SUCCESS', schema: theme };
90
- } catch (e) {
91
- printer.debug(`Failure caught rendering default theme`);
92
- printer.debug(e);
93
- return { resultType: 'FAILURE', schemaName: 'studioTheme', error: e };
128
+ function delay(durationMs: number): Promise<void> {
129
+ return new Promise((r) => setTimeout(() => r(), durationMs));
130
+ }
131
+
132
+ export const waitForSucceededJob = async (getJob: () => Promise<CodegenJob>, { pollInterval }: { pollInterval: number }) => {
133
+ const startTime = performance.now();
134
+ // Adding env variable because if something happens and we need a longer timeout
135
+ // we will give the customer a chance to increase timeout as a workaround.
136
+ // Default timeout is 2 minutes for customers with thousands of components.
137
+ const waitTimeout = process.env.UI_BUILDER_CODEGENJOB_TIMEOUT ? parseInt(process.env.UI_BUILDER_CODEGENJOB_TIMEOUT) : 1000 * 60 * 2;
138
+
139
+ const endTime = startTime + waitTimeout;
140
+
141
+ while (performance.now() < endTime) {
142
+ const job = await getJob();
143
+
144
+ if (!job) {
145
+ printer.error('Codegen job not found');
146
+ throw new Error('Codegen job not found');
147
+ }
148
+
149
+ if (job.status === 'failed') {
150
+ printer.error('Codegen job status is failed', { message: job.statusMessage });
151
+ throw new Error(job.statusMessage);
152
+ }
153
+
154
+ if (job.status === 'succeeded') {
155
+ printer.debug(`Polling time: ${performance.now() - startTime}`);
156
+
157
+ return job;
158
+ }
159
+
160
+ await delay(pollInterval);
94
161
  }
95
- };
96
162
 
97
- /**
98
- * Returns instances of StudioForm from form schemas
99
- */
100
- export const generateUiBuilderForms = (
101
- context: $TSContext,
102
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
- formSchemas: any[],
104
- dataSchema?: GenericDataSchema,
105
- autoGenerateForms?: boolean,
106
- formFeatureFlags?: FormFeatureFlags,
107
- ): CodegenResponse<StudioForm>[] => {
108
- const modelMap: { [model: string]: Set<'create' | 'update'> } = {};
109
- if (dataSchema?.dataSourceType === 'DataStore' && autoGenerateForms) {
110
- Object.entries(dataSchema.models).forEach(([name, model]) => {
111
- if (checkIsSupportedAsForm(model, formFeatureFlags) && !model.isJoinTable) {
112
- modelMap[name] = new Set(['create', 'update']);
113
- }
114
- });
163
+ if (performance.now() > endTime) {
164
+ printer.error(`Codegen job never succeeded before timeout`);
115
165
  }
116
- const codegenForm = (schema: StudioForm): CodegenResponse<StudioForm> => {
166
+
167
+ throw new Error('Failed to return codegen job');
168
+ };
169
+
170
+ export const fetchWithRetries = async (url: string, retries = 3, delay = 300) => {
171
+ let retryCount = 0;
172
+ let retryDelay = delay;
173
+
174
+ while (retryCount < retries) {
117
175
  try {
118
- const form = createUiBuilderForm(context, schema, dataSchema, formFeatureFlags);
119
- return { resultType: 'SUCCESS', schema: form };
120
- } catch (e) {
121
- printer.debug(`Failure caught processing ${schema.name}`);
122
- printer.debug(e);
123
- return {
124
- resultType: 'FAILURE',
125
- schemaName: schema.name,
126
- schema,
127
- error: e,
128
- };
176
+ const response = await fetch(url);
177
+ return response;
178
+ } catch (error) {
179
+ printer.debug(`Error fetching ${url}: ${error}`);
180
+ retryCount = retryCount + 1;
181
+ await new Promise((res) => setTimeout(res, delay));
182
+ retryDelay = retryDelay * 2;
129
183
  }
130
- };
131
- const formResults = formSchemas.map((schema: StudioForm) => {
132
- if (schema?.dataType && schema.dataType?.dataSourceType === 'DataStore') {
133
- modelMap[schema.dataType.dataTypeName]?.delete(schema.formActionType);
184
+ }
185
+ throw new Error('Fetch reached max number of retries without succeeding');
186
+ };
187
+
188
+ export const extractUIComponents = async (url: string, uiBuilderComponentsPath: string) => {
189
+ try {
190
+ if (!fs.existsSync(uiBuilderComponentsPath)) {
191
+ fs.mkdirSync(uiBuilderComponentsPath, { recursive: true });
134
192
  }
135
- return codegenForm(schema);
136
- });
137
193
 
138
- // append remaining models
139
- formResults.push(...generateBaseForms(modelMap).map(codegenForm));
194
+ const response = await fetchWithRetries(url);
195
+ if (!response.ok) {
196
+ throw new Error('Failed to download component manifest file');
197
+ }
198
+ const manifestFile = await (<
199
+ Promise<{ Output: { downloadUrl: string | undefined; fileName: string; schemaName: string; error: string | undefined }[] }>
200
+ >response.json());
201
+
202
+ const downloadComponent = async (output: { fileName: string; downloadUrl: string | undefined; error: string | undefined }) => {
203
+ if (output.downloadUrl && !output.error) {
204
+ try {
205
+ const response = await fetchWithRetries(output.downloadUrl);
206
+ if (!response.ok) {
207
+ printer.debug(`Failed to download ${output.fileName}`);
208
+ throw new Error(`Failed to download ${output.fileName}`);
209
+ }
210
+ return { content: await response.text(), error: undefined, fileName: output.fileName };
211
+ } catch (error) {
212
+ printer.debug(`Skipping ${output.fileName} because of an error downloading the component`);
213
+ return { error: `Failed to download ${output.fileName}`, content: undefined, fileName: output.fileName };
214
+ }
215
+ } else {
216
+ printer.debug(`Skipping ${output.fileName} because of an error generating the component`);
217
+ return { error: output.error, content: undefined, fileName: output.fileName };
218
+ }
219
+ };
140
220
 
141
- printer.debug(
142
- `Generated ${formResults.filter((result) => result.resultType === 'SUCCESS').length} forms in ${getUiBuilderComponentsPath(context)}`,
143
- );
144
- return formResults;
221
+ for await (const downloaded of asyncPool(5, manifestFile.Output, downloadComponent)) {
222
+ if (downloaded.content) {
223
+ fs.writeFileSync(path.join(uiBuilderComponentsPath, downloaded.fileName), downloaded.content);
224
+ printer.debug(`Downloaded ${downloaded.fileName}`);
225
+ }
226
+ }
227
+ printer.debug('ui-components downloaded successfully');
228
+ } catch (error) {
229
+ printer.error('failed to download ui-components');
230
+ throw error;
231
+ }
145
232
  };