@aws-amplify/amplify-util-uibuilder 1.8.5-rds.1 → 1.9.0-cdk228withdata.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 (37) hide show
  1. package/CHANGELOG.md +3 -8
  2. package/lib/clients/amplify-studio-client.d.ts +4 -1
  3. package/lib/clients/amplify-studio-client.d.ts.map +1 -1
  4. package/lib/clients/amplify-studio-client.js +43 -3
  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 +32 -33
  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 -123
  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 +7 -6
  25. package/src/__tests__/__snapshots__/syncAmplifyUiComponents.test.ts.snap +46 -0
  26. package/src/__tests__/detachedFormCleanup.test.ts +7 -5
  27. package/src/__tests__/generateComponents.test.ts +101 -28
  28. package/src/__tests__/storageManager.test.ts +7 -4
  29. package/src/__tests__/syncAmplifyUiComponents.test.ts +98 -96
  30. package/src/clients/amplify-studio-client.ts +44 -4
  31. package/src/commands/generateComponents.ts +43 -55
  32. package/src/commands/utils/codegenResources.ts +1 -226
  33. package/src/commands/utils/detachedFormCleanup.ts +3 -4
  34. package/src/commands/utils/storageManager.ts +3 -7
  35. package/src/commands/utils/syncAmplifyUiBuilderComponents.ts +210 -123
  36. package/tsconfig.tsbuildinfo +1 -1
  37. package/src/__tests__/createUiBuilderComponent.test.ts +0 -90
@@ -1,211 +1,8 @@
1
- /* eslint-disable spellcheck/spell-checker*/
2
- // You have a misspelled word: Renderer on Identifier
3
- import {
4
- StudioTemplateRendererManager,
5
- StudioComponent,
6
- StudioTheme,
7
- StudioTemplateRendererFactory,
8
- StudioTemplateRenderer,
9
- FrameworkOutputManager,
10
- RenderTextComponentResponse,
11
- GenericDataSchema,
12
- getGenericFromDataStore,
13
- StudioForm,
14
- StudioSchema,
15
- FormFeatureFlags,
16
- } from '@aws-amplify/codegen-ui';
17
- import {
18
- AmplifyRenderer,
19
- AmplifyFormRenderer,
20
- ReactThemeStudioTemplateRenderer,
21
- ReactIndexStudioTemplateRenderer,
22
- ModuleKind,
23
- ScriptTarget,
24
- ScriptKind,
25
- UtilTemplateType,
26
- ReactUtilsStudioTemplateRenderer,
27
- ReactThemeStudioTemplateRendererOptions,
28
- } from '@aws-amplify/codegen-ui-react';
1
+ import { GenericDataSchema, getGenericFromDataStore } from '@aws-amplify/codegen-ui';
29
2
  import { printer } from '@aws-amplify/amplify-prompts';
30
3
  import { $TSContext } from '@aws-amplify/amplify-cli-core';
31
- import { getUiBuilderComponentsPath } from './getUiBuilderComponentsPath';
32
4
  import { ModelIntrospectionSchema } from '@aws-amplify/appsync-modelgen-plugin';
33
5
 
34
- const config = {
35
- module: ModuleKind.ES2020,
36
- target: ScriptTarget.ES2020,
37
- script: ScriptKind.JSX,
38
- renderTypeDeclarations: true,
39
- };
40
-
41
- /**
42
- * Writes component file to the work space
43
- */
44
- export const createUiBuilderComponent = (context: $TSContext, schema: StudioComponent, dataSchema?: GenericDataSchema): StudioComponent => {
45
- const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
46
- const rendererFactory = new StudioTemplateRendererFactory(
47
- (component: StudioComponent) => new AmplifyRenderer(component as StudioComponent, config, dataSchema),
48
- );
49
-
50
- const outputPathDir = uiBuilderComponentsPath;
51
-
52
- const rendererManager = new StudioTemplateRendererManager(rendererFactory, {
53
- outputPathDir,
54
- });
55
-
56
- rendererManager.renderSchemaToTemplate(schema);
57
- return schema;
58
- };
59
-
60
- /**
61
- * Writes theme file to the work space
62
- */
63
- export const createUiBuilderTheme = (
64
- context: $TSContext,
65
- schema: StudioTheme,
66
- options?: ReactThemeStudioTemplateRendererOptions,
67
- ): StudioTheme => {
68
- const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
69
- const rendererFactory = new StudioTemplateRendererFactory(
70
- (component: StudioTheme) =>
71
- new ReactThemeStudioTemplateRenderer(component, config, options) as unknown as StudioTemplateRenderer<
72
- unknown,
73
- StudioTheme,
74
- FrameworkOutputManager<unknown>,
75
- RenderTextComponentResponse
76
- >,
77
- );
78
-
79
- const outputPathDir = uiBuilderComponentsPath;
80
-
81
- const rendererManager = new StudioTemplateRendererManager(rendererFactory, {
82
- outputPathDir,
83
- });
84
-
85
- try {
86
- rendererManager.renderSchemaToTemplate(schema);
87
- return schema;
88
- } catch (e) {
89
- printer.debug(e);
90
- printer.debug('Skipping invalid theme with schema');
91
- printer.debug(JSON.stringify(schema, null, 2));
92
- throw e;
93
- }
94
- };
95
-
96
- /**
97
- * Writes form file to the work space
98
- */
99
- export const createUiBuilderForm = (
100
- context: $TSContext,
101
- schema: StudioForm,
102
- dataSchema?: GenericDataSchema,
103
- formFeatureFlags?: FormFeatureFlags,
104
- ): StudioForm => {
105
- const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
106
- const rendererFactory = new StudioTemplateRendererFactory(
107
- (form: StudioForm) =>
108
- new AmplifyFormRenderer(form, dataSchema, config, formFeatureFlags) as unknown as StudioTemplateRenderer<
109
- unknown,
110
- StudioForm,
111
- FrameworkOutputManager<unknown>,
112
- RenderTextComponentResponse
113
- >,
114
- );
115
-
116
- const outputPathDir = uiBuilderComponentsPath;
117
-
118
- const rendererManager = new StudioTemplateRendererManager(rendererFactory, {
119
- outputPathDir,
120
- });
121
-
122
- try {
123
- rendererManager.renderSchemaToTemplate(schema);
124
- return schema;
125
- } catch (e) {
126
- printer.debug(e);
127
- printer.debug('Skipping invalid form with schema');
128
- printer.debug(JSON.stringify(schema, null, 2));
129
- throw e;
130
- }
131
- };
132
-
133
- /**
134
- * Writes index file to the work space
135
- */
136
- export const generateAmplifyUiBuilderIndexFile = (context: $TSContext, schemas: StudioSchema[]): void => {
137
- const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
138
- const rendererFactory = new StudioTemplateRendererFactory(
139
- (schema: StudioSchema[]) =>
140
- new ReactIndexStudioTemplateRenderer(schema, config) as unknown as StudioTemplateRenderer<
141
- unknown,
142
- StudioSchema[],
143
- FrameworkOutputManager<unknown>,
144
- RenderTextComponentResponse
145
- >,
146
- );
147
-
148
- const outputPathDir = uiBuilderComponentsPath;
149
-
150
- const rendererManager = new StudioTemplateRendererManager(rendererFactory, {
151
- outputPathDir,
152
- });
153
-
154
- try {
155
- if (schemas.length) {
156
- rendererManager.renderSchemaToTemplate(schemas);
157
- }
158
- } catch (e) {
159
- printer.debug(e);
160
- printer.debug('Failed to generate component index file');
161
- throw e;
162
- }
163
- };
164
-
165
- type UtilFileChecks = {
166
- hasForms: boolean;
167
- hasViews: boolean;
168
- };
169
-
170
- /**
171
- * Writes utils file to the work space
172
- */
173
- export const generateAmplifyUiBuilderUtilFile = (context: $TSContext, { hasForms, hasViews }: UtilFileChecks): void => {
174
- const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
175
- const rendererFactory = new StudioTemplateRendererFactory(
176
- (utils: UtilTemplateType[]) => new ReactUtilsStudioTemplateRenderer(utils, config),
177
- );
178
-
179
- const outputPathDir = uiBuilderComponentsPath;
180
-
181
- const rendererManager = new StudioTemplateRendererManager(rendererFactory, {
182
- outputPathDir,
183
- });
184
-
185
- const utils = new Set<UtilTemplateType>();
186
-
187
- if (hasForms) {
188
- utils.add('validation');
189
- utils.add('formatter');
190
- utils.add('fetchByPath');
191
- utils.add('processFile');
192
- }
193
-
194
- if (hasViews) {
195
- utils.add('formatter');
196
- }
197
-
198
- try {
199
- if (utils.size) {
200
- rendererManager.renderSchemaToTemplate([...utils]);
201
- }
202
- } catch (e) {
203
- printer.debug(e);
204
- printer.debug('Failed to generate component index file');
205
- throw e;
206
- }
207
- };
208
-
209
6
  /**
210
7
  * If models are available, they will be populated in the models field of the returned object.
211
8
  * If they're not available, it will return undefined
@@ -224,25 +21,3 @@ export const getAmplifyDataSchema = async (context: $TSContext): Promise<Generic
224
21
  return undefined;
225
22
  }
226
23
  };
227
-
228
- /**
229
- * generates base create/update froms from names
230
- */
231
- export const generateBaseForms = (modelMap: { [model: string]: Set<'create' | 'update'> }): StudioForm[] => {
232
- const getSchema = (name: string, type: 'create' | 'update'): StudioForm => ({
233
- name: `${name}${type === 'create' ? 'CreateForm' : 'UpdateForm'}`,
234
- formActionType: type,
235
- dataType: { dataSourceType: 'DataStore', dataTypeName: name },
236
- fields: {},
237
- sectionalElements: {},
238
- style: {},
239
- cta: {},
240
- });
241
-
242
- const schemas: StudioForm[] = [];
243
-
244
- Object.entries(modelMap).forEach(([name, set]) => {
245
- set.forEach((type) => schemas.push(getSchema(name, type)));
246
- });
247
- return schemas;
248
- };
@@ -1,4 +1,3 @@
1
- import { StudioForm, StudioSchema } from '@aws-amplify/codegen-ui';
2
1
  import { Form } from 'aws-sdk/clients/amplifyuibuilder';
3
2
  import { AmplifyStudioClient } from '../../clients';
4
3
  import { printer } from '@aws-amplify/amplify-prompts';
@@ -7,7 +6,7 @@ import asyncPool from 'tiny-async-pool';
7
6
  /**
8
7
  * Does the form's reference DataStore type exist in the list of models
9
8
  */
10
- export const isFormDetachedFromModel = (formSchema: StudioForm | Form, modelNames: Set<string>): boolean => {
9
+ export const isFormDetachedFromModel = (formSchema: Form, modelNames: Set<string>): boolean => {
11
10
  return formSchema.dataType.dataSourceType === 'DataStore' && !modelNames.has(formSchema.dataType.dataTypeName);
12
11
  };
13
12
 
@@ -16,7 +15,7 @@ export const isFormDetachedFromModel = (formSchema: StudioForm | Form, modelName
16
15
  * @param schema The schema to test
17
16
  * @returns boolean
18
17
  */
19
- export const isFormSchemaCustomized = (formSchema: StudioForm | Form): boolean => {
18
+ export const isFormSchemaCustomized = (formSchema: Form): boolean => {
20
19
  const { fields, style, sectionalElements } = formSchema;
21
20
  // Fields and sectionalElements just need an empty check
22
21
  if (!isEmpty({ ...fields, ...sectionalElements })) {
@@ -31,7 +30,7 @@ export const isFormSchemaCustomized = (formSchema: StudioForm | Form): boolean =
31
30
  /**
32
31
  * Is the schema a StudioForm
33
32
  */
34
- export const isStudioForm = (schema: StudioSchema | undefined): schema is StudioForm => {
33
+ export const isStudioForm = (schema: Form | undefined): schema is Form => {
35
34
  if (!schema) return false;
36
35
  return 'formActionType' in schema;
37
36
  };
@@ -1,6 +1,6 @@
1
- import { StudioForm, StudioFormFieldConfig, StudioGenericFieldConfig, StudioSchema } from '@aws-amplify/codegen-ui';
1
+ import { FieldConfig, Form } from 'aws-sdk/clients/amplifyuibuilder';
2
2
 
3
- export const hasStorageField = (form: StudioForm): boolean => {
3
+ export const hasStorageField = (form: Form): boolean => {
4
4
  const result = Object.values(form.fields).some((field) => {
5
5
  return isGenericFieldConfig(field) && field.inputType?.type === 'StorageField';
6
6
  });
@@ -8,10 +8,6 @@ export const hasStorageField = (form: StudioForm): boolean => {
8
8
  return result;
9
9
  };
10
10
 
11
- export const isFormSchema = (schema: StudioSchema): schema is StudioForm => {
12
- return schema && Object.keys(schema).includes('fields');
13
- };
14
-
15
- export const isGenericFieldConfig = (config: StudioFormFieldConfig): config is StudioGenericFieldConfig => {
11
+ export const isGenericFieldConfig = (config: FieldConfig) => {
16
12
  return !Object.keys(config).includes('excluded');
17
13
  };
@@ -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
  };