@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.
- package/CHANGELOG.md +48 -9
- package/lib/clients/amplify-studio-client.d.ts +5 -1
- package/lib/clients/amplify-studio-client.d.ts.map +1 -1
- package/lib/clients/amplify-studio-client.js +54 -8
- package/lib/clients/amplify-studio-client.js.map +1 -1
- package/lib/commands/generateComponents.d.ts.map +1 -1
- package/lib/commands/generateComponents.js +33 -34
- package/lib/commands/generateComponents.js.map +1 -1
- package/lib/commands/utils/codegenResources.d.ts +1 -15
- package/lib/commands/utils/codegenResources.d.ts.map +1 -1
- package/lib/commands/utils/codegenResources.js +1 -122
- package/lib/commands/utils/codegenResources.js.map +1 -1
- package/lib/commands/utils/detachedFormCleanup.d.ts +3 -4
- package/lib/commands/utils/detachedFormCleanup.d.ts.map +1 -1
- package/lib/commands/utils/detachedFormCleanup.js.map +1 -1
- package/lib/commands/utils/storageManager.d.ts +3 -4
- package/lib/commands/utils/storageManager.d.ts.map +1 -1
- package/lib/commands/utils/storageManager.js +1 -5
- package/lib/commands/utils/storageManager.js.map +1 -1
- package/lib/commands/utils/syncAmplifyUiBuilderComponents.d.ts +8 -16
- package/lib/commands/utils/syncAmplifyUiBuilderComponents.d.ts.map +1 -1
- package/lib/commands/utils/syncAmplifyUiBuilderComponents.js +186 -74
- package/lib/commands/utils/syncAmplifyUiBuilderComponents.js.map +1 -1
- package/package.json +28 -19
- package/src/__tests__/__snapshots__/syncAmplifyUiComponents.test.ts.snap +46 -0
- package/src/__tests__/cloneComponentsFromEnv.test.ts +8 -0
- package/src/__tests__/detachedFormCleanup.test.ts +7 -5
- package/src/__tests__/generateComponents.test.ts +187 -15
- package/src/__tests__/getAmplifyDataSchema.test.ts +1 -2
- package/src/__tests__/prePushHandler.test.ts +7 -0
- package/src/__tests__/storageManager.test.ts +7 -4
- package/src/__tests__/syncAmplifyUiComponents.test.ts +106 -95
- package/src/clients/amplify-studio-client.ts +56 -9
- package/src/commands/generateComponents.ts +45 -56
- package/src/commands/utils/codegenResources.ts +1 -225
- package/src/commands/utils/detachedFormCleanup.ts +3 -4
- package/src/commands/utils/storageManager.ts +3 -7
- package/src/commands/utils/syncAmplifyUiBuilderComponents.ts +210 -123
- package/tsconfig.tsbuildinfo +1 -1
- package/src/__tests__/createUiBuilderComponent.test.ts +0 -90
|
@@ -1,145 +1,232 @@
|
|
|
1
1
|
import { printer } from '@aws-amplify/amplify-prompts';
|
|
2
|
-
import {
|
|
2
|
+
import type { GenericDataSchema, GenericDataField, GenericDataRelationshipType } from '@aws-amplify/codegen-ui';
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
119
|
-
return
|
|
120
|
-
} catch (
|
|
121
|
-
printer.debug(`
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
};
|