@aws-amplify/amplify-util-uibuilder 1.6.2 → 1.6.3-graphql15.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 (66) hide show
  1. package/API.md +19 -0
  2. package/CHANGELOG.md +13 -0
  3. package/amplify-plugin.json +3 -1
  4. package/lib/clients/amplify-studio-client.d.ts +1 -0
  5. package/lib/clients/amplify-studio-client.d.ts.map +1 -1
  6. package/lib/clients/amplify-studio-client.js +13 -0
  7. package/lib/clients/amplify-studio-client.js.map +1 -1
  8. package/lib/commands/cloneComponentsFromEnv.js +2 -2
  9. package/lib/commands/cloneComponentsFromEnv.js.map +1 -1
  10. package/lib/commands/generateComponents.d.ts +1 -1
  11. package/lib/commands/generateComponents.d.ts.map +1 -1
  12. package/lib/commands/generateComponents.js +15 -3
  13. package/lib/commands/generateComponents.js.map +1 -1
  14. package/lib/commands/utils/codegenResources.d.ts.map +1 -1
  15. package/lib/commands/utils/codegenResources.js +3 -1
  16. package/lib/commands/utils/codegenResources.js.map +1 -1
  17. package/lib/commands/utils/detachedFormCleanup.d.ts +11 -0
  18. package/lib/commands/utils/detachedFormCleanup.d.ts.map +1 -0
  19. package/lib/commands/utils/detachedFormCleanup.js +47 -0
  20. package/lib/commands/utils/detachedFormCleanup.js.map +1 -0
  21. package/lib/commands/utils/index.d.ts +1 -0
  22. package/lib/commands/utils/index.d.ts.map +1 -1
  23. package/lib/commands/utils/index.js +1 -0
  24. package/lib/commands/utils/index.js.map +1 -1
  25. package/lib/commands/utils/notifyMissingPackages.js +2 -2
  26. package/lib/commands/utils/notifyMissingPackages.js.map +1 -1
  27. package/lib/commands/utils/shouldRenderComponents.d.ts.map +1 -1
  28. package/lib/commands/utils/shouldRenderComponents.js +7 -5
  29. package/lib/commands/utils/shouldRenderComponents.js.map +1 -1
  30. package/lib/commands/utils/syncAmplifyUiBuilderComponents.d.ts.map +1 -1
  31. package/lib/commands/utils/syncAmplifyUiBuilderComponents.js +3 -1
  32. package/lib/commands/utils/syncAmplifyUiBuilderComponents.js.map +1 -1
  33. package/lib/event-handlers/handle-PostPull.js +1 -1
  34. package/lib/event-handlers/handle-PostPull.js.map +1 -1
  35. package/lib/event-handlers/handle-PostPush.js +1 -1
  36. package/lib/event-handlers/handle-PostPush.js.map +1 -1
  37. package/lib/event-handlers/handle-PrePush.d.ts +3 -0
  38. package/lib/event-handlers/handle-PrePush.d.ts.map +1 -0
  39. package/lib/event-handlers/handle-PrePush.js +15 -0
  40. package/lib/event-handlers/handle-PrePush.js.map +1 -0
  41. package/lib/utils/prePushHandler.d.ts +3 -0
  42. package/lib/utils/prePushHandler.d.ts.map +1 -0
  43. package/lib/utils/prePushHandler.js +40 -0
  44. package/lib/utils/prePushHandler.js.map +1 -0
  45. package/package.json +12 -9
  46. package/src/__tests__/createUiBuilderComponent.test.ts +4 -0
  47. package/src/__tests__/detachedFormCleanup.test.ts +78 -0
  48. package/src/__tests__/generateComponents.test.ts +13 -11
  49. package/src/__tests__/handleEvents.test.ts +27 -0
  50. package/src/__tests__/prePushHandler.test.ts +105 -0
  51. package/src/__tests__/shouldRenderComponents.test.ts +48 -35
  52. package/src/__tests__/syncAmplifyUiComponents.test.ts +3 -3
  53. package/src/clients/amplify-studio-client.ts +13 -0
  54. package/src/commands/cloneComponentsFromEnv.ts +1 -1
  55. package/src/commands/generateComponents.ts +30 -4
  56. package/src/commands/utils/codegenResources.ts +3 -1
  57. package/src/commands/utils/detachedFormCleanup.ts +58 -0
  58. package/src/commands/utils/index.ts +1 -0
  59. package/src/commands/utils/notifyMissingPackages.ts +2 -2
  60. package/src/commands/utils/shouldRenderComponents.ts +7 -5
  61. package/src/commands/utils/syncAmplifyUiBuilderComponents.ts +4 -1
  62. package/src/event-handlers/handle-PostPull.ts +1 -1
  63. package/src/event-handlers/handle-PostPush.ts +1 -1
  64. package/src/event-handlers/handle-PrePush.ts +15 -0
  65. package/src/utils/prePushHandler.ts +55 -0
  66. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prePushHandler = void 0;
4
+ const amplify_prompts_1 = require("amplify-prompts");
5
+ const clients_1 = require("../clients");
6
+ const utils_1 = require("../commands/utils");
7
+ const prePushHandler = async (context) => {
8
+ if (!(await (0, utils_1.shouldRenderComponents)(context))) {
9
+ return;
10
+ }
11
+ try {
12
+ const studioClient = await clients_1.AmplifyStudioClient.setClientInfo(context);
13
+ const [formSchemas, localSchema] = await Promise.all([
14
+ studioClient.listForms(),
15
+ context.amplify.invokePluginMethod(context, 'codegen', undefined, 'getModelIntrospection', [context]),
16
+ ]);
17
+ if (!localSchema) {
18
+ amplify_prompts_1.printer.debug('Local schema not found');
19
+ return;
20
+ }
21
+ printDetachedFormsWarning(formSchemas, localSchema);
22
+ }
23
+ catch (error) {
24
+ amplify_prompts_1.printer.debug(error);
25
+ }
26
+ };
27
+ exports.prePushHandler = prePushHandler;
28
+ const printDetachedFormsWarning = (formSchemas, localSchema) => {
29
+ const modelNames = new Set(Object.keys(localSchema.models));
30
+ const detachedCustomForms = [];
31
+ formSchemas.entities.forEach(form => {
32
+ if ((0, utils_1.isFormDetachedFromModel)(form, modelNames) && (0, utils_1.isFormSchemaCustomized)(form)) {
33
+ detachedCustomForms.push(form.name);
34
+ }
35
+ });
36
+ if (detachedCustomForms.length > 0) {
37
+ amplify_prompts_1.printer.warn(`The following form${detachedCustomForms.length === 1 ? '' : 's'} will no longer be available because the connected data model no longer exists: ${detachedCustomForms.join(', ')}`);
38
+ }
39
+ };
40
+ //# sourceMappingURL=prePushHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prePushHandler.js","sourceRoot":"","sources":["../../src/utils/prePushHandler.ts"],"names":[],"mappings":";;;AAEA,qDAA0C;AAE1C,wCAAiD;AACjD,6CAA4G;AAKrG,MAAM,cAAc,GAAG,KAAK,EAAE,OAAmB,EAAiB,EAAE;IACzE,IAAI,CAAC,CAAC,MAAM,IAAA,8BAAsB,EAAC,OAAO,CAAC,CAAC,EAAE;QAC5C,OAAO;KACR;IACD,IAAI;QACF,MAAM,YAAY,GAAG,MAAM,6BAAmB,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QACtE,MAAM,CAAC,WAAW,EAAE,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAE9B;YACpB,YAAY,CAAC,SAAS,EAAE;YACxB,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAChC,OAAO,EACP,SAAS,EACT,SAAS,EACT,uBAAuB,EACvB,CAAC,OAAO,CAAC,CACV;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,WAAW,EAAE;YAChB,yBAAO,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;YACxC,OAAO;SACR;QACD,yBAAyB,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;KACrD;IAAC,OAAO,KAAK,EAAE;QACd,yBAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;KACtB;AACH,CAAC,CAAC;AA3BW,QAAA,cAAc,kBA2BzB;AAEF,MAAM,yBAAyB,GAAG,CAAC,WAAkD,EAAE,WAAmB,EAAQ,EAAE;IAClH,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5D,MAAM,mBAAmB,GAAa,EAAE,CAAC;IAEzC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;QAGlC,IAAI,IAAA,+BAAuB,EAAC,IAAI,EAAE,UAAU,CAAC,IAAI,IAAA,8BAAsB,EAAC,IAAI,CAAC,EAAE;YAC7E,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;SACrC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,mBAAmB,CAAC,MAAM,GAAG,CAAC,EAAE;QAClC,yBAAO,CAAC,IAAI,CAAC,qBAAqB,mBAAmB,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,mFAAmF,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;KACnM;AACH,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@aws-amplify/amplify-util-uibuilder",
3
- "version": "1.6.2",
3
+ "version": "1.6.3-graphql15.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
7
7
  "build": "tsc",
8
- "test": "jest --logHeapUsage"
8
+ "test": "jest --logHeapUsage",
9
+ "extract-api": "ts-node ../../scripts/extract-api.ts"
9
10
  },
10
11
  "author": "Amazon Web Services",
11
12
  "license": "Apache-2.0",
@@ -13,18 +14,20 @@
13
14
  "access": "public"
14
15
  },
15
16
  "dependencies": {
16
- "@aws-amplify/codegen-ui": "2.5.5",
17
- "@aws-amplify/codegen-ui-react": "2.5.5",
18
- "amplify-cli-core": "3.4.0",
19
- "amplify-prompts": "2.6.1",
17
+ "@aws-amplify/codegen-ui": "2.5.6",
18
+ "@aws-amplify/codegen-ui-react": "2.5.6",
19
+ "amplify-cli-core": "3.5.0-graphql15.0",
20
+ "amplify-prompts": "2.6.2-graphql15.0",
20
21
  "aws-sdk": "^2.1233.0",
21
22
  "fs-extra": "^8.1.0",
22
- "ora": "^4.0.3"
23
+ "ora": "^4.0.3",
24
+ "tiny-async-pool": "^2.1.0"
23
25
  },
24
26
  "devDependencies": {
25
27
  "@types/fs-extra": "^8.0.1",
26
28
  "@types/jest": "^26.0.20",
27
- "@types/semver": "^7.1.0"
29
+ "@types/semver": "^7.1.0",
30
+ "@types/tiny-async-pool": "^2.0.0"
28
31
  },
29
32
  "jest": {
30
33
  "testRunner": "jest-circus/runner",
@@ -68,5 +71,5 @@
68
71
  "usePathForSuiteName": "true",
69
72
  "addFileAttribute": "true"
70
73
  },
71
- "gitHead": "256b180a8194463898cb71035441bad1f43472b2"
74
+ "gitHead": "a870ec62056ad90d3fdd4e85749158d981e8a7af"
72
75
  }
@@ -69,6 +69,10 @@ describe('can create a ui builder component', () => {
69
69
  generateAmplifyUiBuilderIndexFile(context, [schema]);
70
70
  expect(new codegenMock.StudioTemplateRendererManager().renderSchemaToTemplate).toBeCalled();
71
71
  });
72
+ it('does not call renderSchemaToTemplate for index file if no schema', () => {
73
+ generateAmplifyUiBuilderIndexFile(context, []);
74
+ expect(new codegenMock.StudioTemplateRendererManager().renderSchemaToTemplate).not.toBeCalled();
75
+ })
72
76
  it('calls the renderManager for utils file w/ validation, formatter, and fetchByPath helpers if there is a form', () => {
73
77
  generateAmplifyUiBuilderUtilFile(context, { hasForms: true, hasViews: false });
74
78
  expect(new codegenMock.StudioTemplateRendererManager().renderSchemaToTemplate).toBeCalledWith(expect.arrayContaining(['validation', 'formatter', 'fetchByPath']));
@@ -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');
@@ -145,7 +145,9 @@ export const generateAmplifyUiBuilderIndexFile = (context: $TSContext, schemas:
145
145
  });
146
146
 
147
147
  try {
148
- rendererManager.renderSchemaToTemplate(schemas);
148
+ if(schemas.length) {
149
+ rendererManager.renderSchemaToTemplate(schemas);
150
+ }
149
151
  } catch (e) {
150
152
  printer.debug(e);
151
153
  printer.debug('Failed to generate component index file');