From e12116aebf2c65e16b64914d754dca5ee59dc5f1 Mon Sep 17 00:00:00 2001 From: Albert Winberg Date: Tue, 8 Aug 2023 05:33:49 +0000 Subject: [PATCH] fix: use related fields for has_one and belongs_to properties when building GraphQL API payloads --- ...studio-ui-codegen-react-forms.test.ts.snap | 124 ++++++++++-------- .../studio-ui-codegen-react-forms.test.ts | 6 +- .../lib/amplify-ui-renderers/form.ts | 8 +- .../forms/form-renderer-helper/all-props.ts | 4 +- .../event-handler-props.ts | 26 +++- .../form-renderer-helper/model-fields.ts | 45 +++++-- .../render-array-field.ts | 21 ++- .../lib/react-component-renderer.ts | 2 + .../utils/forms/storage-field-component.ts | 9 +- 9 files changed, 160 insertions(+), 85 deletions(-) diff --git a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap index e3cdc47e2..7f9362e20 100644 --- a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap +++ b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap @@ -285,7 +285,7 @@ export default function CreateOwnerForm(props) { event.preventDefault(); let modelFields = { name, - Dog, + ownerDogId: Dog.id, }; const validationResponses = await Promise.all( Object.keys(validations).reduce((promises, fieldName) => { @@ -388,7 +388,7 @@ export default function CreateOwnerForm(props) { if (onChange) { const modelFields = { name: value, - Dog, + ownerDogId: Dog.id, }; const result = onChange(modelFields); value = result?.name ?? value; @@ -1530,7 +1530,7 @@ export default function MyMemberForm(props) { let modelFields = { name, teamID, - Team, + teamMembersId: Team.id, }; const validationResponses = await Promise.all( Object.keys(validations).reduce((promises, fieldName) => { @@ -1637,7 +1637,7 @@ export default function MyMemberForm(props) { const modelFields = { name: value, teamID, - Team, + teamMembersId: Team.id, }; const result = onChange(modelFields); value = result?.name ?? value; @@ -1660,7 +1660,7 @@ export default function MyMemberForm(props) { const modelFields = { name, teamID: value, - Team, + teamMembersId: Team.id, }; const result = onChange(modelFields); value = result?.teamID ?? value; @@ -2693,7 +2693,7 @@ export default function BookCreateForm(props) { event.preventDefault(); let modelFields = { name, - primaryAuthor, + authorId: primaryAuthor.id, }; const validationResponses = await Promise.all( Object.keys(validations).reduce((promises, fieldName) => { @@ -2799,7 +2799,7 @@ export default function BookCreateForm(props) { if (onChange) { const modelFields = { name: value, - primaryAuthor, + authorId: primaryAuthor.id, }; const result = onChange(modelFields); value = result?.name ?? value; @@ -3907,8 +3907,8 @@ export default function BookCreateForm(props) { event.preventDefault(); let modelFields = { name, - primaryAuthor, - primaryTitle, + authorId: primaryAuthor.id, + titleId: primaryTitle.id, }; const validationResponses = await Promise.all( Object.keys(validations).reduce((promises, fieldName) => { @@ -4014,8 +4014,8 @@ export default function BookCreateForm(props) { if (onChange) { const modelFields = { name: value, - primaryAuthor, - primaryTitle, + authorId: primaryAuthor.id, + titleId: primaryTitle.id, }; const result = onChange(modelFields); value = result?.name ?? value; @@ -4038,7 +4038,7 @@ export default function BookCreateForm(props) { const modelFields = { name, primaryAuthor: value, - primaryTitle, + titleId: primaryTitle.id, }; const result = onChange(modelFields); value = result?.primaryAuthor ?? value; @@ -4120,7 +4120,7 @@ export default function BookCreateForm(props) { if (onChange) { const modelFields = { name, - primaryAuthor, + authorId: primaryAuthor.id, primaryTitle: value, }; const result = onChange(modelFields); @@ -6864,7 +6864,7 @@ export default function CommentUpdateForm(props) { let modelFields = { content, postID, - Post, + postCommentsId: Post.id, post: post1, }; const validationResponses = await Promise.all( @@ -6941,7 +6941,7 @@ export default function CommentUpdateForm(props) { const modelFields = { content: value, postID, - Post, + postCommentsId: Post.id, post: post1, }; const result = onChange(modelFields); @@ -6965,7 +6965,7 @@ export default function CommentUpdateForm(props) { const modelFields = { content, postID: value, - Post, + postCommentsId: Post.id, post: post1, }; const result = onChange(modelFields); @@ -7124,7 +7124,7 @@ export default function CommentUpdateForm(props) { const modelFields = { content, postID, - Post, + postCommentsId: Post.id, post: value, }; const result = onChange(modelFields); @@ -7585,7 +7585,7 @@ export default function CommentUpdateForm(props) { let modelFields = { content, postID, - Post, + postCommentsId: Post.id, post: post1, }; const validationResponses = await Promise.all( @@ -7662,7 +7662,7 @@ export default function CommentUpdateForm(props) { const modelFields = { content: value, postID, - Post, + postCommentsId: Post.id, post: post1, }; const result = onChange(modelFields); @@ -7686,7 +7686,7 @@ export default function CommentUpdateForm(props) { const modelFields = { content, postID: value, - Post, + postCommentsId: Post.id, post: post1, }; const result = onChange(modelFields); @@ -7845,7 +7845,7 @@ export default function CommentUpdateForm(props) { const modelFields = { content, postID, - Post, + postCommentsId: Post.id, post: value, }; const result = onChange(modelFields); @@ -9006,7 +9006,7 @@ export default function UpdateCPKTeacherForm(props) { event.preventDefault(); let modelFields = { specialTeacherId, - CPKStudent, + cPKTeacherCPKStudentSpecialStudentId: CPKStudent.specialStudentId, CPKClasses, CPKProjects, }; @@ -9230,7 +9230,7 @@ export default function UpdateCPKTeacherForm(props) { if (onChange) { const modelFields = { specialTeacherId: value, - CPKStudent, + cPKTeacherCPKStudentSpecialStudentId: CPKStudent.specialStudentId, CPKClasses, CPKProjects, }; @@ -9333,7 +9333,7 @@ export default function UpdateCPKTeacherForm(props) { if (onChange) { const modelFields = { specialTeacherId, - CPKStudent, + cPKTeacherCPKStudentSpecialStudentId: CPKStudent.specialStudentId, CPKClasses: values, CPKProjects, }; @@ -9409,7 +9409,7 @@ export default function UpdateCPKTeacherForm(props) { if (onChange) { const modelFields = { specialTeacherId, - CPKStudent, + cPKTeacherCPKStudentSpecialStudentId: CPKStudent.specialStudentId, CPKClasses, CPKProjects: values, }; @@ -10730,9 +10730,9 @@ export default function CreateCommentForm(props) { event.preventDefault(); let modelFields = { name, - post, - User, - Org, + postID: post.id, + userCommentsId: User.id, + orgCommentsId: Org.id, postCommentsId, }; const validationResponses = await Promise.all( @@ -10805,9 +10805,9 @@ export default function CreateCommentForm(props) { if (onChange) { const modelFields = { name: value, - post, - User, - Org, + postID: post.id, + userCommentsId: User.id, + orgCommentsId: Org.id, postCommentsId, }; const result = onChange(modelFields); @@ -10831,8 +10831,8 @@ export default function CreateCommentForm(props) { const modelFields = { name, post: value, - User, - Org, + userCommentsId: User.id, + orgCommentsId: Org.id, postCommentsId, }; const result = onChange(modelFields); @@ -10906,9 +10906,9 @@ export default function CreateCommentForm(props) { if (onChange) { const modelFields = { name, - post, + postID: post.id, User: value, - Org, + orgCommentsId: Org.id, postCommentsId, }; const result = onChange(modelFields); @@ -10982,8 +10982,8 @@ export default function CreateCommentForm(props) { if (onChange) { const modelFields = { name, - post, - User, + postID: post.id, + userCommentsId: User.id, Org: value, postCommentsId, }; @@ -11058,9 +11058,9 @@ export default function CreateCommentForm(props) { if (onChange) { const modelFields = { name, - post, - User, - Org, + postID: post.id, + userCommentsId: User.id, + orgCommentsId: Org.id, postCommentsId: value, }; const result = onChange(modelFields); @@ -11694,8 +11694,10 @@ export default function CreateCompositeDogForm(props) { let modelFields = { name, description, - CompositeBowl, - CompositeOwner, + compositeDogCompositeBowlShape: CompositeBowl.shape, + compositeDogCompositeBowlSize: CompositeBowl.size, + compositeDogCompositeOwnerLastName: CompositeOwner.lastName, + compositeDogCompositeOwnerFirstName: CompositeOwner.firstName, CompositeToys, CompositeVets, }; @@ -11843,8 +11845,10 @@ export default function CreateCompositeDogForm(props) { const modelFields = { name: value, description, - CompositeBowl, - CompositeOwner, + compositeDogCompositeBowlShape: CompositeBowl.shape, + compositeDogCompositeBowlSize: CompositeBowl.size, + compositeDogCompositeOwnerLastName: CompositeOwner.lastName, + compositeDogCompositeOwnerFirstName: CompositeOwner.firstName, CompositeToys, CompositeVets, }; @@ -11872,8 +11876,10 @@ export default function CreateCompositeDogForm(props) { const modelFields = { name, description: value, - CompositeBowl, - CompositeOwner, + compositeDogCompositeBowlShape: CompositeBowl.shape, + compositeDogCompositeBowlSize: CompositeBowl.size, + compositeDogCompositeOwnerLastName: CompositeOwner.lastName, + compositeDogCompositeOwnerFirstName: CompositeOwner.firstName, CompositeToys, CompositeVets, }; @@ -11899,7 +11905,8 @@ export default function CreateCompositeDogForm(props) { name, description, CompositeBowl: value, - CompositeOwner, + compositeDogCompositeOwnerLastName: CompositeOwner.lastName, + compositeDogCompositeOwnerFirstName: CompositeOwner.firstName, CompositeToys, CompositeVets, }; @@ -11982,7 +11989,8 @@ export default function CreateCompositeDogForm(props) { const modelFields = { name, description, - CompositeBowl, + compositeDogCompositeBowlShape: CompositeBowl.shape, + compositeDogCompositeBowlSize: CompositeBowl.size, CompositeOwner: value, CompositeToys, CompositeVets, @@ -12065,8 +12073,10 @@ export default function CreateCompositeDogForm(props) { const modelFields = { name, description, - CompositeBowl, - CompositeOwner, + compositeDogCompositeBowlShape: CompositeBowl.shape, + compositeDogCompositeBowlSize: CompositeBowl.size, + compositeDogCompositeOwnerLastName: CompositeOwner.lastName, + compositeDogCompositeOwnerFirstName: CompositeOwner.firstName, CompositeToys: values, CompositeVets, }; @@ -12148,8 +12158,10 @@ export default function CreateCompositeDogForm(props) { const modelFields = { name, description, - CompositeBowl, - CompositeOwner, + compositeDogCompositeBowlShape: CompositeBowl.shape, + compositeDogCompositeBowlSize: CompositeBowl.size, + compositeDogCompositeOwnerLastName: CompositeOwner.lastName, + compositeDogCompositeOwnerFirstName: CompositeOwner.firstName, CompositeToys, CompositeVets: values, }; @@ -13729,7 +13741,7 @@ export default function CreateDogForm(props) { event.preventDefault(); let modelFields = { name, - owner, + dogOwnerId: owner.id, }; const validationResponses = await Promise.all( Object.keys(validations).reduce((promises, fieldName) => { @@ -13825,7 +13837,7 @@ export default function CreateDogForm(props) { if (onChange) { const modelFields = { name: value, - owner, + dogOwnerId: owner.id, }; const result = onChange(modelFields); value = result?.name ?? value; @@ -14270,7 +14282,7 @@ export default function CreateOwnerForm(props) { event.preventDefault(); let modelFields = { name, - Dog, + ownerDogId: Dog.id, }; const validationResponses = await Promise.all( Object.keys(validations).reduce((promises, fieldName) => { @@ -14373,7 +14385,7 @@ export default function CreateOwnerForm(props) { if (onChange) { const modelFields = { name: value, - Dog, + ownerDogId: Dog.id, }; const result = onChange(modelFields); value = result?.name ?? value; diff --git a/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts index 2208825fa..f165f7fe2 100644 --- a/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts +++ b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts @@ -1001,9 +1001,9 @@ describe('amplify form renderer tests', () => { ); expect(componentText).toContain('postCommentsId'); - expect(componentText).not.toContain('postID'); - expect(componentText).not.toContain('userCommentsId'); - expect(componentText).not.toContain('orgCommentsId'); + expect(componentText).toContain('postID'); + expect(componentText).toContain('userCommentsId'); + expect(componentText).toContain('orgCommentsId'); expect(componentText).toMatchSnapshot(); expect(declaration).toMatchSnapshot(); }); diff --git a/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts b/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts index 237fb1660..f49c4c8e0 100644 --- a/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts +++ b/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts @@ -323,7 +323,13 @@ export default class FormRenderer extends ReactComponentRenderer, valueNameOverride?: Identifier, + models?: Record, + isRenderingGraphQL = false, ): IfStatement => { const keyPath = fieldName.split('.'); const keyName = keyPath[0]; @@ -229,9 +232,15 @@ export const buildOverrideOnChangeStatement = ( factory.createIdentifier('onChange'), factory.createBlock( [ - buildModelFieldObject(true, fieldConfigs, { - [keyName]: keyValueExpression, - }), + buildModelFieldObject( + true, + fieldConfigs, + models || {}, + { + [keyName]: keyValueExpression, + }, + isRenderingGraphQL, + ), factory.createVariableStatement( undefined, factory.createVariableDeclarationList( @@ -286,7 +295,9 @@ function getCallbackVarName(fieldType: string): string { export const buildOnChangeStatement = ( component: StudioComponent | StudioComponentChild, fieldConfigs: Record, - dataApi?: DataApiKind, + dataApi: DataApiKind | undefined, + models: Record = {}, + isRenderingGraphQL = false, ) => { const { name: fieldName, componentType: fieldType } = component; const fieldConfig = fieldConfigs[fieldName]; @@ -309,7 +320,9 @@ export const buildOnChangeStatement = ( } if (!shouldWrapInArrayField(fieldConfig)) { - handleChangeStatements.push(buildOverrideOnChangeStatement(fieldName, fieldConfigs)); + handleChangeStatements.push( + buildOverrideOnChangeStatement(fieldName, fieldConfigs, undefined, models, isRenderingGraphQL), + ); } handleChangeStatements.push(getOnChangeValidationBlock(fieldName)); @@ -515,6 +528,7 @@ export const buildStorageManagerOnChangeStatement = ( component: StudioComponent | StudioComponentChild, fieldConfigs: Record, handlerName: 'onUploadSuccess' | 'onFileRemove', + isRenderingGraphQL: boolean, ) => { const { name: fieldName } = component; const fieldConfig = fieldConfigs[fieldName]; @@ -581,7 +595,7 @@ export const buildStorageManagerOnChangeStatement = ( NodeFlags.Let, ), ), - buildOverrideOnChangeStatement(fieldName, fieldConfigs), + buildOverrideOnChangeStatement(fieldName, fieldConfigs, undefined, undefined, isRenderingGraphQL), factory.createReturnStatement(factory.createIdentifier('value')), ], true, diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/model-fields.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/model-fields.ts index ffe73d7bb..0fbad3760 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/model-fields.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/model-fields.ts @@ -14,7 +14,7 @@ limitations under the License. */ import { factory, NodeFlags, ObjectLiteralElementLike } from 'typescript'; -import { FieldConfigMetadata } from '@aws-amplify/codegen-ui'; +import { FieldConfigMetadata, GenericDataModel } from '@aws-amplify/codegen-ui'; /** * builds modelFields object which is used to validate, onSubmit, onSuccess/onError @@ -33,30 +33,49 @@ import { FieldConfigMetadata } from '@aws-amplify/codegen-ui'; export const buildModelFieldObject = ( shouldBeConst: boolean, fieldConfigs: Record = {}, + models: Record = {}, nameOverrides: Record = {}, + isRenderingGraphQL = false, ) => { const fieldSet = new Set(); const fields = Object.keys(fieldConfigs).reduce((acc, value) => { const fieldName = value.split('.')[0]; - const { sanitizedFieldName } = fieldConfigs[value]; + const { sanitizedFieldName, relationship } = fieldConfigs[value]; const renderedFieldName = sanitizedFieldName || fieldName; if (!fieldSet.has(renderedFieldName)) { - let assignment: ObjectLiteralElementLike = factory.createShorthandPropertyAssignment( - factory.createIdentifier(fieldName), - undefined, - ); - + let assignments: ObjectLiteralElementLike[] = [ + factory.createShorthandPropertyAssignment(factory.createIdentifier(fieldName), undefined), + ]; if (nameOverrides[fieldName]) { - assignment = nameOverrides[fieldName]; + assignments = [nameOverrides[fieldName]]; + } else if ( + isRenderingGraphQL && + typeof fieldConfigs[value].dataType === 'object' && + relationship && + (relationship.type === 'BELONGS_TO' || relationship.type === 'HAS_ONE') + ) { + assignments = + relationship.associatedFields?.map((associatedField, index) => { + return factory.createPropertyAssignment( + factory.createStringLiteral(associatedField), + factory.createPropertyAccessChain( + factory.createIdentifier(fieldName), + undefined, + models[relationship.relatedModelName].primaryKeys[index], + ), + ); + }) || []; } else if (sanitizedFieldName) { // if overrides present, ignore sanitizedFieldName - assignment = factory.createPropertyAssignment( - factory.createStringLiteral(fieldName), - factory.createIdentifier(sanitizedFieldName), - ); + assignments = [ + factory.createPropertyAssignment( + factory.createStringLiteral(fieldName), + factory.createIdentifier(sanitizedFieldName), + ), + ]; } - acc.push(assignment); + acc.push(...assignments); fieldSet.add(renderedFieldName); } return acc; diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-array-field.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-array-field.ts index a6f5d3e31..61f762c16 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-array-field.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-array-field.ts @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { FieldConfigMetadata, LabelDecorator } from '@aws-amplify/codegen-ui'; +import { FieldConfigMetadata, GenericDataModel, LabelDecorator } from '@aws-amplify/codegen-ui'; import { Expression, factory, Identifier, JsxAttribute, JsxChild, NodeFlags, SyntaxKind } from 'typescript'; import { buildAccessChain, @@ -38,12 +38,16 @@ function getOnChangeAttribute({ renderedFieldName, fieldConfigs, isLimitedToOneValue, + models, + isRenderingGraphQL, }: { setStateName: Identifier; fieldName: string; renderedFieldName: string; fieldConfigs: Record; isLimitedToOneValue?: boolean; + models: Record | undefined; + isRenderingGraphQL: boolean; }): JsxAttribute { const fieldConfig = fieldConfigs[fieldName]; const { dataType, componentType } = fieldConfig; @@ -97,7 +101,7 @@ function getOnChangeAttribute({ NodeFlags.Let, ), ), - buildOverrideOnChangeStatement(fieldName, fieldConfigs, valueName), + buildOverrideOnChangeStatement(fieldName, fieldConfigs, valueName, models, isRenderingGraphQL), ...setStateStatements, ], true, @@ -132,6 +136,7 @@ export const renderArrayFieldComponent = ( labelDecorator?: LabelDecorator, isRequired?: boolean, dataApi: DataApiKind = 'DataStore', + models: Record = {}, ) => { const fieldConfig = fieldConfigs[fieldName]; const { sanitizedFieldName, dataType, componentType } = fieldConfig; @@ -172,7 +177,17 @@ export const renderArrayFieldComponent = ( ); } - props.push(getOnChangeAttribute({ fieldName, isLimitedToOneValue, fieldConfigs, renderedFieldName, setStateName })); + props.push( + getOnChangeAttribute({ + fieldName, + isLimitedToOneValue, + fieldConfigs, + renderedFieldName, + setStateName, + models, + isRenderingGraphQL: dataApi === 'GraphQL', + }), + ); let labelAttribute = factory.createJsxAttribute( factory.createIdentifier('label'), factory.createJsxExpression(undefined, factory.createStringLiteral(fieldLabel)), diff --git a/packages/codegen-ui-react/lib/react-component-renderer.ts b/packages/codegen-ui-react/lib/react-component-renderer.ts index e778c1561..7c64c2cb2 100644 --- a/packages/codegen-ui-react/lib/react-component-renderer.ts +++ b/packages/codegen-ui-react/lib/react-component-renderer.ts @@ -98,6 +98,7 @@ export class ReactComponentRenderer extends ComponentRendererBase< fieldConfigs, labelDecorator, isRequired, + this.importCollection.rendererConfig?.apiConfiguration?.dataApi === 'GraphQL', ); } @@ -118,6 +119,7 @@ export class ReactComponentRenderer extends ComponentRendererBase< labelDecorator, isRequired, this.importCollection.rendererConfig?.apiConfiguration?.dataApi, + this.componentMetadata.dataSchemaMetadata?.models, ); } } diff --git a/packages/codegen-ui-react/lib/utils/forms/storage-field-component.ts b/packages/codegen-ui-react/lib/utils/forms/storage-field-component.ts index c1a479ad9..ce7ea1dbd 100644 --- a/packages/codegen-ui-react/lib/utils/forms/storage-field-component.ts +++ b/packages/codegen-ui-react/lib/utils/forms/storage-field-component.ts @@ -324,6 +324,7 @@ export const renderStorageFieldComponent = ( fieldConfigs: Record, labelDecorator?: LabelDecorator, isRequired?: boolean, + isRenderingGraphQL = false, ) => { const { name: componentName } = component; const dataTypeName = componentMetadata.formMetadata?.dataType.dataTypeName || ''; @@ -418,8 +419,12 @@ export const renderStorageFieldComponent = ( ); } - storageManagerAttributes.push(buildStorageManagerOnChangeStatement(component, fieldConfigs, 'onUploadSuccess')); - storageManagerAttributes.push(buildStorageManagerOnChangeStatement(component, fieldConfigs, 'onFileRemove')); + storageManagerAttributes.push( + buildStorageManagerOnChangeStatement(component, fieldConfigs, 'onUploadSuccess', isRenderingGraphQL), + ); + storageManagerAttributes.push( + buildStorageManagerOnChangeStatement(component, fieldConfigs, 'onFileRemove', isRenderingGraphQL), + ); storageManagerAttributes.push( factory.createJsxAttribute( factory.createIdentifier('processFile'),