@aws-amplify/amplify-util-uibuilder 1.6.5 → 1.6.6-rc.4b3db16df.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 (57) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/amplify-plugin.json +3 -1
  3. package/lib/clients/amplify-studio-client.d.ts +1 -0
  4. package/lib/clients/amplify-studio-client.d.ts.map +1 -1
  5. package/lib/clients/amplify-studio-client.js +13 -0
  6. package/lib/clients/amplify-studio-client.js.map +1 -1
  7. package/lib/commands/cloneComponentsFromEnv.js +2 -2
  8. package/lib/commands/cloneComponentsFromEnv.js.map +1 -1
  9. package/lib/commands/generateComponents.d.ts +1 -1
  10. package/lib/commands/generateComponents.d.ts.map +1 -1
  11. package/lib/commands/generateComponents.js +15 -3
  12. package/lib/commands/generateComponents.js.map +1 -1
  13. package/lib/commands/utils/detachedFormCleanup.d.ts +11 -0
  14. package/lib/commands/utils/detachedFormCleanup.d.ts.map +1 -0
  15. package/lib/commands/utils/detachedFormCleanup.js +44 -0
  16. package/lib/commands/utils/detachedFormCleanup.js.map +1 -0
  17. package/lib/commands/utils/index.d.ts +1 -0
  18. package/lib/commands/utils/index.d.ts.map +1 -1
  19. package/lib/commands/utils/index.js +1 -0
  20. package/lib/commands/utils/index.js.map +1 -1
  21. package/lib/commands/utils/shouldRenderComponents.d.ts.map +1 -1
  22. package/lib/commands/utils/shouldRenderComponents.js +7 -5
  23. package/lib/commands/utils/shouldRenderComponents.js.map +1 -1
  24. package/lib/commands/utils/syncAmplifyUiBuilderComponents.d.ts.map +1 -1
  25. package/lib/commands/utils/syncAmplifyUiBuilderComponents.js +3 -1
  26. package/lib/commands/utils/syncAmplifyUiBuilderComponents.js.map +1 -1
  27. package/lib/event-handlers/handle-PostPull.js +1 -1
  28. package/lib/event-handlers/handle-PostPull.js.map +1 -1
  29. package/lib/event-handlers/handle-PostPush.js +1 -1
  30. package/lib/event-handlers/handle-PostPush.js.map +1 -1
  31. package/lib/event-handlers/handle-PrePush.d.ts +3 -0
  32. package/lib/event-handlers/handle-PrePush.d.ts.map +1 -0
  33. package/lib/event-handlers/handle-PrePush.js +15 -0
  34. package/lib/event-handlers/handle-PrePush.js.map +1 -0
  35. package/lib/utils/prePushHandler.d.ts +3 -0
  36. package/lib/utils/prePushHandler.d.ts.map +1 -0
  37. package/lib/utils/prePushHandler.js +35 -0
  38. package/lib/utils/prePushHandler.js.map +1 -0
  39. package/package.json +8 -6
  40. package/src/__tests__/detachedFormCleanup.test.ts +78 -0
  41. package/src/__tests__/generateComponents.test.ts +13 -11
  42. package/src/__tests__/handleEvents.test.ts +27 -0
  43. package/src/__tests__/prePushHandler.test.ts +105 -0
  44. package/src/__tests__/shouldRenderComponents.test.ts +48 -35
  45. package/src/__tests__/syncAmplifyUiComponents.test.ts +3 -3
  46. package/src/clients/amplify-studio-client.ts +13 -0
  47. package/src/commands/cloneComponentsFromEnv.ts +1 -1
  48. package/src/commands/generateComponents.ts +30 -4
  49. package/src/commands/utils/detachedFormCleanup.ts +55 -0
  50. package/src/commands/utils/index.ts +1 -0
  51. package/src/commands/utils/shouldRenderComponents.ts +7 -5
  52. package/src/commands/utils/syncAmplifyUiBuilderComponents.ts +3 -1
  53. package/src/event-handlers/handle-PostPull.ts +1 -1
  54. package/src/event-handlers/handle-PostPush.ts +1 -1
  55. package/src/event-handlers/handle-PrePush.ts +15 -0
  56. package/src/utils/prePushHandler.ts +51 -0
  57. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,78 @@
1
+ // @aws-amplify/codegen-ui is in project dependencies
2
+ import { StudioSchema } from '@aws-amplify/codegen-ui'; // eslint-disable-line import/no-extraneous-dependencies
3
+ import {AmplifyStudioClient} from '../clients'; // eslint-disable-line import/no-extraneous-dependencies
4
+ import {
5
+ isFormDetachedFromModel, isFormSchemaCustomized, isStudioForm, deleteDetachedForms
6
+ } from '../commands/utils';
7
+
8
+ const amplifyStudioClientMock = AmplifyStudioClient as any;
9
+
10
+ const formWithNoCustomization: StudioSchema = {
11
+ id: 'f-123456',
12
+ name: 'BlogCreateForm',
13
+ formActionType: 'create',
14
+ dataType: {
15
+ dataSourceType: 'DataStore',
16
+ dataTypeName: 'Blog',
17
+ },
18
+ fields: {},
19
+ sectionalElements: {},
20
+ style: {},
21
+ cta: {},
22
+ };
23
+
24
+ const formWithStyle: StudioSchema = {
25
+ ...formWithNoCustomization,
26
+ dataType: { dataSourceType: 'Custom', dataTypeName: 'Blog' },
27
+ style: {
28
+ horizontalGap: { value: '20px' },
29
+ },
30
+ };
31
+
32
+ const formWithFields: StudioSchema = {
33
+ ...formWithNoCustomization,
34
+ dataType: { dataSourceType: 'Custom', dataTypeName: 'Blog' },
35
+ fields: {
36
+ name: { inputType: { type: 'TextField' } },
37
+ },
38
+ };
39
+
40
+ describe('detachedFormCleanup', () => {
41
+
42
+ it('isStudioForm should return true', () => {
43
+ expect(isStudioForm(formWithNoCustomization)).toBe(true);
44
+ });
45
+
46
+ it('isStudioForm should return false', () => {
47
+ expect(isStudioForm(undefined)).toBe(false);
48
+ });
49
+
50
+ it('isFormSchemaCustomized should return false', () => {
51
+ expect(isFormSchemaCustomized(formWithNoCustomization)).toBe(false);
52
+ });
53
+
54
+ it('isFormSchemaCustomized should return true', () => {
55
+ expect(isFormSchemaCustomized(formWithStyle)).toBe(true);
56
+ expect(isFormSchemaCustomized(formWithFields)).toBe(true);
57
+ });
58
+
59
+ it('isFormDetachedFromModel should return true for a DataStore form with detached model', () => {
60
+ expect(isFormDetachedFromModel(formWithNoCustomization, new Set<string>())).toBe(true);
61
+ });
62
+
63
+ it('isFormDetachedFromModel should return false for a Custom form', () => {
64
+ expect(isFormDetachedFromModel(formWithStyle, new Set<string>())).toBe(false);
65
+ });
66
+
67
+ it('isFormDetachedFromModel should return false for a DataStore form with an attached model', () => {
68
+ expect(isFormDetachedFromModel(formWithNoCustomization, new Set<string>().add('Blog'))).toBe(false);
69
+ });
70
+
71
+ it('should delete detached forms', async () => {
72
+ const mockedDeleteForm = jest.fn().mockReturnValue(undefined);
73
+ amplifyStudioClientMock.deleteForm = mockedDeleteForm;
74
+ await deleteDetachedForms([{id: '12345', name: 'BlogCreateForm'}], amplifyStudioClientMock);
75
+ expect(mockedDeleteForm).toBeCalledTimes(1);
76
+ expect(mockedDeleteForm).toHaveBeenCalledWith('12345');
77
+ });
78
+ });
@@ -9,8 +9,10 @@ const utilsMock = utils as any;
9
9
 
10
10
  utilsMock.shouldRenderComponents = jest.fn().mockReturnValue(true);
11
11
  utilsMock.notifyMissingPackages = jest.fn().mockReturnValue(true);
12
+ utilsMock.getAmplifyDataSchema = jest.fn().mockReturnValue(({}));
13
+
12
14
  jest.mock('../commands/utils/featureFlags', () => ({
13
- getTransformerVersion: jest.fn().mockImplementation(() => 2),
15
+ getTransformerVersion: jest.fn().mockReturnValue(2),
14
16
  }));
15
17
 
16
18
  describe('can generate components', () => {
@@ -37,6 +39,13 @@ describe('can generate components', () => {
37
39
  name: 'testSchema',
38
40
  schemaVersion: '1.0',
39
41
  },
42
+ {
43
+ resultType: 'FAILURE',
44
+ schemaName: 'testSchema',
45
+ name: 'testSchema',
46
+ schemaVersion: '1.0',
47
+ schema: { id: 'f-123456', name: 'testSchema' },
48
+ },
40
49
  ],
41
50
  };
42
51
  mockedExport = jest.fn().mockReturnValue({
@@ -70,22 +79,15 @@ describe('can generate components', () => {
70
79
  utilsMock.getAmplifyDataSchema = jest.fn().mockReturnValue(undefined);
71
80
  utilsMock.generateAmplifyUiBuilderIndexFile = jest.fn().mockReturnValue(true);
72
81
  utilsMock.generateAmplifyUiBuilderUtilFile = jest.fn().mockReturnValue(true);
82
+ utilsMock.deleteDetachedForms = jest.fn();
73
83
  });
74
84
 
75
85
  it('runs generateComponents', async () => {
76
- await run(context);
86
+ await run(context, 'PostPull');
77
87
  expect(mockedExport).toBeCalledTimes(3);
78
88
  expect(utilsMock.generateUiBuilderComponents).toBeCalledTimes(1);
79
89
  expect(utilsMock.generateUiBuilderThemes).toBeCalledTimes(1);
80
90
  expect(utilsMock.generateUiBuilderForms).toBeCalledTimes(1);
81
- });
82
-
83
- it('does not run generateComponents if not Amplify Admin app', async () => {
84
- utilsMock.shouldRenderComponents = jest.fn().mockReturnValue(false);
85
- await run(context);
86
- expect(mockedExport).toBeCalledTimes(0);
87
- expect(utilsMock.generateUiBuilderComponents).toBeCalledTimes(0);
88
- expect(utilsMock.generateUiBuilderThemes).toBeCalledTimes(0);
89
- expect(utilsMock.generateUiBuilderForms).toBeCalledTimes(0);
91
+ expect(utilsMock.deleteDetachedForms).toBeCalledTimes(1);
90
92
  });
91
93
  });
@@ -0,0 +1,27 @@
1
+ import { run as runPostPull } from '../event-handlers/handle-PostPull';
2
+ import { run as runPostPush } from '../event-handlers/handle-PostPush';
3
+ import { run as generateComponents } from '../commands/generateComponents';
4
+
5
+ jest.mock('../commands/generateComponents');
6
+
7
+ describe('can handle events', () => {
8
+ let context: any;
9
+
10
+ beforeEach(() => {
11
+ context = {
12
+ amplify: {
13
+ invokePluginMethod: jest.fn(),
14
+ },
15
+ };
16
+ });
17
+
18
+ it('handles postPull', async () => {
19
+ await runPostPull(context);
20
+ expect(generateComponents).toHaveBeenCalledWith(context, 'PostPull');
21
+ });
22
+
23
+ it('handles postPush', async () => {
24
+ await runPostPush(context);
25
+ expect(generateComponents).toHaveBeenCalledWith(context, 'PostPush');
26
+ });
27
+ });
@@ -0,0 +1,105 @@
1
+ // aws-sdk, amplify-cli-core, amplify-prompts are in package dependencies
2
+ import aws from 'aws-sdk'; // eslint-disable-line import/no-extraneous-dependencies
3
+ import { Form } from 'aws-sdk/clients/amplifyuibuilder'; // eslint-disable-line import/no-extraneous-dependencies
4
+ import { printer } from 'amplify-prompts'; // eslint-disable-line import/no-extraneous-dependencies
5
+ import * as utils from '../commands/utils';
6
+ import { prePushHandler } from '../utils/prePushHandler';
7
+
8
+ jest.mock('../commands/utils');
9
+ jest.mock('amplify-cli-core');
10
+ jest.mock('../commands/utils/featureFlags', () => ({
11
+ getTransformerVersion: jest.fn().mockImplementation(() => 2),
12
+ }));
13
+
14
+ const awsMock = aws as any;
15
+ const utilsMock = utils as any;
16
+
17
+ utilsMock.shouldRenderComponents = jest.fn().mockImplementation(() => true);
18
+
19
+ describe('handlePrePush', () => {
20
+ let context: any;
21
+ let mockedExport: jest.Mock<any, any>;
22
+ let exportedForms: Form[];
23
+
24
+ beforeEach(() => {
25
+ context = {
26
+ amplify: {
27
+ invokePluginMethod: () => ({ models: { Comment: {} } }),
28
+ },
29
+ input: {
30
+ options: {
31
+ appId: 'testAppId',
32
+ envName: 'testEnvName',
33
+ },
34
+ },
35
+ };
36
+
37
+ exportedForms = [
38
+ {
39
+ name: 'BlogCreateForm',
40
+ formActionType: 'create',
41
+ fields: {},
42
+ style: {},
43
+ schemaVersion: '1.0',
44
+ sectionalElements: {},
45
+ appId: '1234',
46
+ environmentName: 'staging',
47
+ dataType: {
48
+ dataSourceType: 'DataStore',
49
+ dataTypeName: 'Blog',
50
+ },
51
+ id: '12345',
52
+ },
53
+ {
54
+ name: 'BlogUpdateForm',
55
+ formActionType: 'update',
56
+ fields: {},
57
+ style: {},
58
+ schemaVersion: '1.0',
59
+ sectionalElements: {},
60
+ appId: '1234',
61
+ environmentName: 'staging',
62
+ dataType: {
63
+ dataSourceType: 'DataStore',
64
+ dataTypeName: 'Blog',
65
+ },
66
+ id: '12345',
67
+ },
68
+ ];
69
+
70
+ mockedExport = jest.fn(() => ({
71
+ entities: exportedForms,
72
+ }));
73
+
74
+ awsMock.AmplifyUIBuilder = jest.fn(() => ({
75
+ exportForms: jest.fn(() => ({
76
+ promise: () => mockedExport(),
77
+ })),
78
+ getMetadata: jest.fn(() => ({
79
+ promise: jest.fn(() => ({
80
+ features: {
81
+ autoGenerateForms: 'true',
82
+ autoGenerateViews: 'true',
83
+ },
84
+ })),
85
+ })),
86
+ }));
87
+ });
88
+
89
+ it('runs handlePrePush', async () => {
90
+ utilsMock.isFormDetachedFromModel = jest.fn().mockImplementation(() => true);
91
+ utilsMock.isFormSchemaCustomized = jest.fn().mockImplementation(() => true);
92
+ const spy = jest.spyOn(printer, 'warn');
93
+ await prePushHandler(context);
94
+ expect(spy).toHaveBeenCalledWith('The following forms will no longer be available because the connected data model no longer exists: BlogCreateForm, BlogUpdateForm');
95
+ });
96
+
97
+ it('runs handlePrePush without schema', async () => {
98
+ context.amplify.invokePluginMethod = jest.fn()
99
+ .mockImplementation(() => ({ models: { Comment: {} } }))
100
+ .mockImplementation(() => (null));
101
+ const spy = jest.spyOn(printer, 'debug');
102
+ await prePushHandler(context);
103
+ expect(spy).toHaveBeenCalledWith('Local schema not found');
104
+ });
105
+ });
@@ -1,7 +1,5 @@
1
1
  // eslint-disable-next-line import/no-extraneous-dependencies
2
- import {
3
- $TSContext, AmplifyCategories, AmplifySupportedService, CloudformationProviderFacade,
4
- } from 'amplify-cli-core';
2
+ import {$TSContext} from 'amplify-cli-core';
5
3
  import aws from 'aws-sdk'; // eslint-disable-line import/no-extraneous-dependencies
6
4
  import { shouldRenderComponents } from '../commands/utils/shouldRenderComponents';
7
5
 
@@ -10,26 +8,21 @@ const awsMock = aws as any;
10
8
  jest.mock('amplify-cli-core', () => ({
11
9
  ...jest.requireActual('amplify-cli-core'),
12
10
  stateManager: {
13
- getMeta: jest.fn(() => ({
14
- [AmplifyCategories.API]: {
15
- MyResourceName: {
16
- service: AmplifySupportedService.APPSYNC,
17
- },
11
+ getProjectConfig: jest.fn().mockReturnValue({
12
+ providers: ['awscloudformation'],
13
+ frontend: 'javascript',
14
+ javascript: {
15
+ framework: 'react',
18
16
  },
19
- })),
20
- // eslint-disable-next-line spellcheck/spell-checker
21
- getCLIJSON: jest.fn().mockReturnValue({ features: { graphqltransformer: { transformerversion: 2 } } }),
22
- setCLIJSON: jest.fn(),
23
- },
24
- FeatureFlags: {
25
- getBoolean: () => true,
26
- getNumber: jest.fn().mockReturnValue(2),
27
- reloadValues: jest.fn(),
17
+ })
28
18
  },
29
19
  }));
30
- CloudformationProviderFacade.isAmplifyAdminApp = jest.fn().mockReturnValue({
31
- isAdminApp: true,
32
- });
20
+
21
+ jest.mock('../clients', () => ({
22
+ AmplifyStudioClient: {
23
+ isAmplifyApp: jest.fn().mockReturnValue(true)
24
+ }
25
+ }));
33
26
 
34
27
  describe('should render components', () => {
35
28
  let context: $TSContext | any;
@@ -47,9 +40,6 @@ describe('should render components', () => {
47
40
  })),
48
41
  }));
49
42
  context = {
50
- amplify: {
51
- invokePluginMethod: () => ({}),
52
- },
53
43
  input: {
54
44
  options: {
55
45
  'no-codegen': false,
@@ -57,38 +47,61 @@ describe('should render components', () => {
57
47
  envName: 'testEnvName',
58
48
  },
59
49
  },
60
- exeInfo: {
61
- projectConfig: {
62
- providers: ['awscloudformation'],
63
- frontend: 'javascript',
64
- javascript: {
65
- framework: 'react',
66
- },
67
- },
68
- },
69
50
  };
70
51
  });
52
+
71
53
  it('works with a valid config', async () => {
72
54
  const shouldIt = await shouldRenderComponents(context);
73
55
  expect(shouldIt).toBe(true);
74
56
  });
57
+
75
58
  it("doesn't work if --no-codegen flag is set", async () => {
76
59
  context.input.options['no-codegen'] = true;
77
60
  const shouldIt = await shouldRenderComponents(context);
78
61
  expect(shouldIt).toBe(false);
79
62
  });
63
+
80
64
  it("doesn't work if provider is not awscloudformation", async () => {
81
- context.exeInfo.projectConfig.providers = [];
65
+ jest.mock('amplify-cli-core', () => ({
66
+ stateManager: {
67
+ getProjectConfig: jest.fn().mockReturnValue({
68
+ providers: [],
69
+ frontend: 'javascript',
70
+ javascript: {
71
+ framework: 'react',
72
+ },
73
+ })
74
+ }
75
+ }));
82
76
  const shouldIt = await shouldRenderComponents(context);
83
77
  expect(shouldIt).toBe(false);
84
78
  });
79
+
85
80
  it('should return false if frontend is ios', async () => {
86
- context.exeInfo.projectConfig.frontend = 'ios';
81
+ jest.mock('amplify-cli-core', () => ({
82
+ stateManager: {
83
+ getProjectConfig: jest.fn().mockReturnValue({
84
+ providers: [],
85
+ frontend: 'ios',
86
+ })
87
+ },
88
+ }));
87
89
  const shouldIt = await shouldRenderComponents(context);
88
90
  expect(shouldIt).toBe(false);
89
91
  });
92
+
90
93
  it('should return false if frontend is vue', async () => {
91
- context.exeInfo.projectConfig.javascript.framework = 'vue';
94
+ jest.mock('amplify-cli-core', () => ({
95
+ stateManager: {
96
+ getProjectConfig: jest.fn().mockReturnValue({
97
+ providers: [],
98
+ frontend: 'javascript',
99
+ javascript: {
100
+ framework: 'vue',
101
+ },
102
+ })
103
+ },
104
+ }));
92
105
  const shouldIt = await shouldRenderComponents(context);
93
106
  expect(shouldIt).toBe(false);
94
107
  });
@@ -262,7 +262,7 @@ describe('should sync amplify ui builder components', () => {
262
262
  const forms = generateUiBuilderForms(context, [{}, {}]);
263
263
  expect(forms.every(form => form.resultType === 'FAILURE')).toBeTruthy();
264
264
  });
265
- it('can generate uibuilder forms from data schema if autogen true', async () => {
265
+ it('can generate uibuilder forms from data schema if autogenerate true', async () => {
266
266
  createUiBuilderComponentDependencyMock.createUiBuilderForm = jest.fn().mockImplementation(() => ({}));
267
267
  const forms = generateUiBuilderForms(context, [], exampleSchema, true);
268
268
  expect(forms.every(form => form.resultType === 'SUCCESS')).toBeTruthy();
@@ -270,7 +270,7 @@ describe('should sync amplify ui builder components', () => {
270
270
  expect(forms.length).toEqual(2);
271
271
  });
272
272
 
273
- it('should not autogen forms for join tables or unsupported models', async () => {
273
+ it('should not autogenerate forms for join tables or unsupported models', async () => {
274
274
  expect(Object.keys(exampleSchema.models)).toStrictEqual(['Author', 'JoinTable', 'EmptyModel']);
275
275
  createUiBuilderComponentDependencyMock.createUiBuilderForm = jest.fn().mockImplementation(
276
276
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -282,7 +282,7 @@ describe('should sync amplify ui builder components', () => {
282
282
  expect(forms.map(f => (f.schema as any).name)).toStrictEqual(['Author', 'Author']);
283
283
  });
284
284
 
285
- it('does not generate uibuilder forms from data schema if autogen false', async () => {
285
+ it('does not generate uibuilder forms from data schema if autogenerate false', async () => {
286
286
  createUiBuilderComponentDependencyMock.createUiBuilderForm = jest.fn().mockImplementation(() => ({}));
287
287
  const forms = generateUiBuilderForms(context, [], exampleSchema, false);
288
288
  expect(forms.length).toEqual(0);
@@ -230,6 +230,19 @@ export default class AmplifyStudioClient {
230
230
  }
231
231
  };
232
232
 
233
+ deleteForm = async (formId: string, envName?: string, appId?: string): Promise<void> => {
234
+ const environmentName = envName || this.#envName;
235
+ const resolvedAppId = appId || this.#appId;
236
+ try {
237
+ await this.#amplifyUiBuilder
238
+ .deleteForm({ id: formId, environmentName, appId: resolvedAppId })
239
+ .promise();
240
+ } catch (err) {
241
+ printer.debug(err.toString());
242
+ throw err;
243
+ }
244
+ }
245
+
233
246
  getModels = async (resourceName: string, envName?: string, appId?: string): Promise<string | undefined> => {
234
247
  try {
235
248
  const environmentName = envName || this.#envName;
@@ -11,7 +11,7 @@ export const run = async (context: $TSContext): Promise<void> => {
11
11
  const args = extractArgs(context);
12
12
  const sourceEnvName = args.sourceEnvName ? args.sourceEnvName : context.exeInfo.sourceEnvName;
13
13
  const newEnvName = args.newEnvName ? args.newEnvName : context.exeInfo.localEnvInfo.envName;
14
- const appId = args.appId ?? context.exeInfo.teamProviderInfo[sourceEnvName].awscloudformation.AmplifyAppId;
14
+ const appId = args.appId ?? context.exeInfo?.teamProviderInfo?.[sourceEnvName]?.awscloudformation?.AmplifyAppId;
15
15
  const studioClient = await AmplifyStudioClient.setClientInfo(context, sourceEnvName, appId);
16
16
 
17
17
  const [existingComponents, existingComponentsNewEnv] = await Promise.all([
@@ -13,12 +13,15 @@ import {
13
13
  generateAmplifyUiBuilderIndexFile,
14
14
  generateUiBuilderForms,
15
15
  generateAmplifyUiBuilderUtilFile,
16
+ isStudioForm,
17
+ isFormDetachedFromModel,
18
+ deleteDetachedForms,
16
19
  } from './utils';
17
20
 
18
21
  /**
19
22
  * Pulls ui components from Studio backend and generates the code in the user's file system
20
23
  */
21
- export const run = async (context: $TSContext): Promise<void> => {
24
+ export const run = async (context: $TSContext, eventType: 'PostPush' | 'PostPull'): Promise<void> => {
22
25
  if (!(await shouldRenderComponents(context))) {
23
26
  return;
24
27
  }
@@ -32,9 +35,9 @@ export const run = async (context: $TSContext): Promise<void> => {
32
35
  getAmplifyDataSchema(studioClient),
33
36
  ]);
34
37
 
35
- const nothingWouldAutogen = !dataSchema || !studioClient.metadata.autoGenerateForms || !studioClient.isGraphQLSupported;
38
+ const nothingWouldAutogenerate = !dataSchema || !studioClient.metadata.autoGenerateForms || !studioClient.isGraphQLSupported;
36
39
 
37
- if (nothingWouldAutogen && [componentSchemas, themeSchemas, formSchemas].every(group => !group.entities.length)) {
40
+ if (nothingWouldAutogenerate && [componentSchemas, themeSchemas, formSchemas].every(group => !group.entities.length)) {
38
41
  printer.debug('Skipping UI component generation since none are found.');
39
42
  return;
40
43
  }
@@ -44,13 +47,18 @@ export const run = async (context: $TSContext): Promise<void> => {
44
47
  component: generateUiBuilderComponents(context, componentSchemas.entities, dataSchema),
45
48
  theme: generateUiBuilderThemes(context, themeSchemas.entities),
46
49
  form: generateUiBuilderForms(
47
- context, formSchemas.entities, dataSchema, studioClient.metadata.autoGenerateForms && studioClient.isGraphQLSupported,
50
+ context,
51
+ formSchemas.entities,
52
+ dataSchema,
53
+ studioClient.metadata.autoGenerateForms && studioClient.isGraphQLSupported,
48
54
  ),
49
55
  };
50
56
 
51
57
  const successfulSchemas: StudioSchema[] = [];
58
+ const detachedForms: {id: string, name: string}[] = [];
52
59
  let hasSuccessfulForm = false;
53
60
  const failedResponseNames: string[] = [];
61
+ const modelNames = dataSchema?.models ? new Set(Object.keys(dataSchema.models)) : new Set<string>();
54
62
 
55
63
  Object.entries(generatedResults).forEach(([key, results]) => {
56
64
  results.forEach(result => {
@@ -60,6 +68,22 @@ export const run = async (context: $TSContext): Promise<void> => {
60
68
  hasSuccessfulForm = true;
61
69
  }
62
70
  } else {
71
+ const failedSchema = result.schema;
72
+ /**
73
+ * A form resource may fail to generate because it's DataStore model type
74
+ * no longer exists.
75
+ */
76
+ if (
77
+ isStudioForm(failedSchema)
78
+ && failedSchema.id
79
+ && dataSchema
80
+ && eventType === 'PostPush'
81
+ && isFormDetachedFromModel(failedSchema, modelNames)
82
+ ) {
83
+ // Don't need to add form to failedResponseNames if it is going to be deleted
84
+ detachedForms.push({id: failedSchema.id, name: failedSchema.name});
85
+ return;
86
+ }
63
87
  failedResponseNames.push(result.schemaName);
64
88
  }
65
89
  });
@@ -88,6 +112,8 @@ export const run = async (context: $TSContext): Promise<void> => {
88
112
  }
89
113
 
90
114
  notifyMissingPackages(context);
115
+
116
+ deleteDetachedForms(detachedForms, studioClient);
91
117
  } catch (e) {
92
118
  printer.debug(e);
93
119
  spinner.fail('Failed to sync UI components');
@@ -0,0 +1,55 @@
1
+ import { StudioForm, StudioSchema } from '@aws-amplify/codegen-ui';
2
+ import { Form } from 'aws-sdk/clients/amplifyuibuilder';
3
+ import { AmplifyStudioClient } from '../../clients';
4
+ import { printer } from 'amplify-prompts';
5
+ import asyncPool from 'tiny-async-pool';
6
+
7
+ /**
8
+ * Does the form's reference DataStore type exist in the list of models
9
+ */
10
+ export const isFormDetachedFromModel = (formSchema: StudioForm | Form, modelNames: Set<string>): boolean => {
11
+ return formSchema.dataType.dataSourceType === 'DataStore' && !modelNames.has(formSchema.dataType.dataTypeName);
12
+ };
13
+
14
+ /**
15
+ * A form schema will be customized if fields, style, sectionalElements objects aren't empty
16
+ * @param schema The schema to test
17
+ * @returns boolean
18
+ */
19
+ export const isFormSchemaCustomized = (formSchema: StudioForm | Form): boolean => {
20
+ const { fields, style, sectionalElements } = formSchema;
21
+ // Fields and sectionalElements just need an empty check
22
+ if (!isEmpty({ ...fields, ...sectionalElements })) {
23
+ return true;
24
+ }
25
+
26
+ // Check style
27
+ // If not empty, at least one style must be defined
28
+ return Object.values(style).some(styleConfig => styleConfig !== undefined);
29
+ };
30
+
31
+ /**
32
+ * Is the schema a StudioForm
33
+ */
34
+ export const isStudioForm = (schema: StudioSchema | undefined): schema is StudioForm => {
35
+ if (!schema) return false;
36
+ return 'formActionType' in schema;
37
+ };
38
+
39
+ export const deleteDetachedForms = async (detachedForms: {id: string, name: string}[], studioClient: AmplifyStudioClient) => {
40
+ const deleteForm = async ({id, name}: {id: string, name: string}) => {
41
+ try {
42
+ // Try to delete the form but don't stop any other operations if it fails
43
+ await studioClient.deleteForm(id);
44
+ return {status: 'SUCCESS', message: `Deleted detached form ${name}`};
45
+ } catch (error) {
46
+ return {status: 'FAIL', message: `Failed to delete detached form ${name}`}
47
+ }
48
+ };
49
+ for await (const status of asyncPool(5, detachedForms, deleteForm)) {
50
+ printer.debug(status.message);
51
+ };
52
+ };
53
+
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ const isEmpty = (obj?: any): boolean => Object.keys(obj).length === 0;
@@ -5,3 +5,4 @@ export * from './getUiBuilderComponentsPath';
5
5
  export * from './notifyMissingPackages';
6
6
  export * from './shouldRenderComponents';
7
7
  export * from './syncAmplifyUiBuilderComponents';
8
+ export * from './detachedFormCleanup';
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable prefer-arrow/prefer-arrow-functions */
2
- import { $TSContext } from 'amplify-cli-core';
2
+ import { $TSContext, stateManager } from 'amplify-cli-core';
3
3
  import { printer } from 'amplify-prompts';
4
4
  import { AmplifyStudioClient } from '../../clients';
5
5
  import { getAppId } from './environmentHelpers';
@@ -8,6 +8,8 @@ import { getAppId } from './environmentHelpers';
8
8
  * process to decide if we should render components
9
9
  */
10
10
  export const shouldRenderComponents = async (context: $TSContext): Promise<boolean> => {
11
+ const projectConfig = stateManager.getProjectConfig();
12
+
11
13
  if (process.env.FORCE_RENDER) {
12
14
  printer.debug('Forcing component render since environment variable flag is set.');
13
15
  return true;
@@ -17,22 +19,22 @@ export const shouldRenderComponents = async (context: $TSContext): Promise<boole
17
19
  return false;
18
20
  }
19
21
 
20
- if (!context?.exeInfo?.projectConfig) {
22
+ if (!projectConfig) {
21
23
  printer.debug('Not pulling components because there is no projectConfig set for this project.');
22
24
  return false;
23
25
  }
24
26
 
25
- if (!context.exeInfo.projectConfig.providers.includes('awscloudformation')) {
27
+ if (!projectConfig.providers.includes('awscloudformation')) {
26
28
  printer.debug('Not pulling components because there is no "awscloudformation" provider.');
27
29
  return false;
28
30
  }
29
31
 
30
- if (context.exeInfo.projectConfig.frontend !== 'javascript') {
32
+ if (projectConfig.frontend !== 'javascript') {
31
33
  printer.debug('Not pulling components because this project is not configured as a javascript frontend.');
32
34
  return false;
33
35
  }
34
36
 
35
- if (context.exeInfo.projectConfig.javascript.framework !== 'react') {
37
+ if (projectConfig.javascript.framework !== 'react') {
36
38
  printer.debug('Not pulling components because this project is not configured with the "react" framework.');
37
39
  return false;
38
40
  }
@@ -120,7 +120,9 @@ export const generateUiBuilderForms = (
120
120
  } catch (e) {
121
121
  printer.debug(`Failure caught processing ${schema.name}`);
122
122
  printer.debug(e);
123
- return { resultType: 'FAILURE', schemaName: schema.name, error: e };
123
+ return {
124
+ resultType: 'FAILURE', schemaName: schema.name, schema, error: e,
125
+ };
124
126
  }
125
127
  };
126
128
  const formResults = formSchemas.map((schema: StudioForm) => {
@@ -7,7 +7,7 @@ import { run as generateComponents } from '../commands/generateComponents';
7
7
  */
8
8
  export const run = async (context: $TSContext): Promise<void> => {
9
9
  try {
10
- await generateComponents(context);
10
+ await generateComponents(context, 'PostPull');
11
11
  } catch (e) {
12
12
  // Swallow all errors from the uibuilder plugin
13
13
  printer.debug(e);
@@ -7,7 +7,7 @@ import { run as generateComponents } from '../commands/generateComponents';
7
7
  */
8
8
  export const run = async (context: $TSContext): Promise<void> => {
9
9
  try {
10
- await generateComponents(context);
10
+ await generateComponents(context, 'PostPush');
11
11
  } catch (e) {
12
12
  // Swallow all errors from the uibuilder plugin
13
13
  printer.debug(e);