From fa408c84c3ecf0f13f5bed07007e3a4009f038dd Mon Sep 17 00:00:00 2001 From: charles shin Date: Wed, 18 Jan 2023 13:20:58 -0800 Subject: [PATCH 1/3] fix: throw validation error when updating models with misconfigured hasMany relationship --- package-lock.json | 5 +- .../SchoolUpdateForm.jsx | 4 + ...studio-ui-codegen-react-forms.test.ts.snap | 98 ++++----- .../studio-ui-codegen-react-forms.test.ts | 11 + .../forms/form-renderer-helper/cta-props.ts | 3 +- .../forms/form-renderer-helper/form-state.ts | 2 + .../form-renderer-helper/relationship.ts | 194 ++++++++---------- .../lib/forms/react-form-renderer.ts | 9 +- .../codegen-ui-react/lib/helpers/index.ts | 18 ++ .../__tests__/generic-from-datastore.test.ts | 12 +- .../codegen-ui/lib/generic-from-datastore.ts | 6 + packages/codegen-ui/lib/types/data.ts | 1 + 12 files changed, 198 insertions(+), 165 deletions(-) diff --git a/package-lock.json b/package-lock.json index e88ee18c2..123deb614 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18460,7 +18460,7 @@ "dev": true, "requires": { "is-ssh": "^1.3.0", - "parse-url": "^6.0.0" + "parse-url": ">=6.0.1" } }, "git-url-parse": { @@ -21457,7 +21457,8 @@ } }, "parse-url": { - "version": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", "dev": true, "requires": { diff --git a/packages/codegen-ui-golden-files/lib/react/forms/datastore-update-form-with-has-many-relationship/SchoolUpdateForm.jsx b/packages/codegen-ui-golden-files/lib/react/forms/datastore-update-form-with-has-many-relationship/SchoolUpdateForm.jsx index 73ca2ca77..774148a8f 100644 --- a/packages/codegen-ui-golden-files/lib/react/forms/datastore-update-form-with-has-many-relationship/SchoolUpdateForm.jsx +++ b/packages/codegen-ui-golden-files/lib/react/forms/datastore-update-form-with-has-many-relationship/SchoolUpdateForm.jsx @@ -164,6 +164,7 @@ export default function SchoolUpdateForm(props) { const [schoolRecord, setSchoolRecord] = React.useState(school); const [linkedStudents, setLinkedStudents] = React.useState([]); + const canUnlinkStudents = false; React.useEffect(() => { const queryData = async () => { @@ -252,6 +253,9 @@ export default function SchoolUpdateForm(props) { const studentsToUnLink = []; const studentsSet = new Set(); const linkedStudentsSet = new Set(); + if (!canUnlinkStudents && studentsToUnLink.length > 0) { + throw Error(`${original.id} cannot be unlinked from School because schoolID is a required field.`); + } Students.forEach((r) => studentsSet.add(r.id)); linkedStudents.forEach((r) => linkedStudentsSet.add(r.id)); 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 fe2f63a77..7db6bb8a8 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 @@ -3514,7 +3514,9 @@ export default function UpdateCompositeDogForm(props) { const [compositeDogRecord, setCompositeDogRecord] = React.useState(compositeDog); const [linkedCompositeToys, setLinkedCompositeToys] = React.useState([]); + const canUnlinkCompositeToys = true; const [linkedCompositeVets, setLinkedCompositeVets] = React.useState([]); + const canUnlinkCompositeVets = false; React.useEffect(() => { const queryData = async () => { const record = nameProp @@ -3748,6 +3750,11 @@ export default function UpdateCompositeDogForm(props) { const compositeToysToUnLink = []; const compositeToysSet = new Set(); const linkedCompositeToysSet = new Set(); + if (!canUnlinkCompositeToys && compositeToysToUnLink.length > 0) { + throw Error( + \`\${original.id} cannot be unlinked from CompositeDog because compositeDogCompositeToysName is a required field.\` + ); + } CompositeToys.forEach((r) => compositeToysSet.add(getIDValue.CompositeToys?.(r)) ); @@ -3765,27 +3772,14 @@ export default function UpdateCompositeDogForm(props) { } }); compositeToysToUnLink.forEach((original) => { - try { - promises.push( - DataStore.save( - CompositeToy.copyOf(original, (updated) => { - updated.compositeDogCompositeToysName = null; - updated.compositeDogCompositeToysDescription = null; - }) - ) - ); - } catch (err) { - if ( - err.message === - \\"Field compositeDogCompositeToysName is required\\" - ) { - throw Error( - \`\${original.id} cannot be unlinked from CompositeDog because compositeDogCompositeToysName is a required field.\` - ); - } else { - throw err; - } - } + promises.push( + DataStore.save( + CompositeToy.copyOf(original, (updated) => { + updated.compositeDogCompositeToysName = null; + updated.compositeDogCompositeToysDescription = null; + }) + ) + ); }); compositeToysToLink.forEach((original) => { promises.push( @@ -4596,7 +4590,9 @@ export default function UpdateCPKTeacherForm(props) { }; const [cPKTeacherRecord, setCPKTeacherRecord] = React.useState(cPKTeacher); const [linkedCPKClasses, setLinkedCPKClasses] = React.useState([]); + const canUnlinkCPKClasses = false; const [linkedCPKProjects, setLinkedCPKProjects] = React.useState([]); + const canUnlinkCPKProjects = true; React.useEffect(() => { const queryData = async () => { const record = specialTeacherIdProp @@ -4829,6 +4825,11 @@ export default function UpdateCPKTeacherForm(props) { const cPKProjectsToUnLink = []; const cPKProjectsSet = new Set(); const linkedCPKProjectsSet = new Set(); + if (!canUnlinkCPKProjects && cPKProjectsToUnLink.length > 0) { + throw Error( + \`\${original.id} cannot be unlinked from CPKTeacher because cPKTeacherID is a required field.\` + ); + } CPKProjects.forEach((r) => cPKProjectsSet.add(getIDValue.CPKProjects?.(r)) ); @@ -4846,23 +4847,13 @@ export default function UpdateCPKTeacherForm(props) { } }); cPKProjectsToUnLink.forEach((original) => { - try { - promises.push( - DataStore.save( - CPKProject.copyOf(original, (updated) => { - updated.cPKTeacherID = null; - }) - ) - ); - } catch (err) { - if (err.message === \\"Field cPKTeacherID is required\\") { - throw Error( - \`\${original.id} cannot be unlinked from CPKTeacher because cPKTeacherID is a required field.\` - ); - } else { - throw err; - } - } + promises.push( + DataStore.save( + CPKProject.copyOf(original, (updated) => { + updated.cPKTeacherID = null; + }) + ) + ); }); cPKProjectsToLink.forEach((original) => { promises.push( @@ -11347,6 +11338,7 @@ export default function SchoolUpdateForm(props) { }; const [schoolRecord, setSchoolRecord] = React.useState(school); const [linkedStudents, setLinkedStudents] = React.useState([]); + const canUnlinkStudents = false; React.useEffect(() => { const queryData = async () => { const record = idProp ? await DataStore.query(School, idProp) : school; @@ -11450,6 +11442,11 @@ export default function SchoolUpdateForm(props) { const studentsToUnLink = []; const studentsSet = new Set(); const linkedStudentsSet = new Set(); + if (!canUnlinkStudents && studentsToUnLink.length > 0) { + throw Error( + \`\${original.id} cannot be unlinked from School because schoolID is a required field.\` + ); + } Students.forEach((r) => studentsSet.add(getIDValue.Students?.(r))); linkedStudents.forEach((r) => linkedStudentsSet.add(getIDValue.Students?.(r)) @@ -11465,23 +11462,13 @@ export default function SchoolUpdateForm(props) { } }); studentsToUnLink.forEach((original) => { - try { - promises.push( - DataStore.save( - Student.copyOf(original, (updated) => { - updated.schoolID = null; - }) - ) - ); - } catch (err) { - if (err.message === \\"Field schoolID is required\\") { - throw Error( - \`\${original.id} cannot be unlinked from School because schoolID is a required field.\` - ); - } else { - throw err; - } - } + promises.push( + DataStore.save( + Student.copyOf(original, (updated) => { + updated.schoolID = null; + }) + ) + ); }); studentsToLink.forEach((original) => { promises.push( @@ -12387,6 +12374,7 @@ export default function TagUpdateForm(props) { }; const [tagRecord, setTagRecord] = React.useState(tag); const [linkedPosts, setLinkedPosts] = React.useState([]); + const canUnlinkPosts = false; React.useEffect(() => { const queryData = async () => { const record = idProp ? await DataStore.query(Tag, idProp) : tag; 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 a29af49fd..96f8e89f4 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 @@ -464,6 +464,17 @@ describe('amplify form renderer tests', () => { expect(declaration).toMatchSnapshot(); }); + it('should render an update form with validation for misconfigured schema for hasMany relationship', () => { + const { componentText } = generateWithAmplifyFormRenderer( + 'forms/school-datastore-update', + 'datastore/school-student', + undefined, + { isNonModelSupported: true, isRelationshipSupported: true }, + ); + + expect(componentText).toContain('const canUnlinkStudents = false'); + }); + it('should render an update form for model with composite keys', () => { const { componentText, declaration } = generateWithAmplifyFormRenderer( 'forms/composite-dog-datastore-update', diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/cta-props.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/cta-props.ts index fa3f798dd..e2f32a374 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/cta-props.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/cta-props.ts @@ -23,6 +23,7 @@ import { VariableStatement, ExpressionStatement, PropertyAssignment, + IfStatement, } from 'typescript'; import { getSetNameIdentifier, lowerCaseFirst } from '../../helpers'; import { getDisplayValueObjectName } from './model-values'; @@ -205,7 +206,7 @@ export const buildDataStoreExpression = ( ) => { const thisModelPrimaryKeys = dataSchema.models[modelName].primaryKeys; // promises.push(...statements that handle hasMany/ manyToMany/ hasOne-belongsTo relationships) - const relationshipsPromisesAccessStatements: (VariableStatement | ExpressionStatement)[] = []; + const relationshipsPromisesAccessStatements: (VariableStatement | ExpressionStatement | IfStatement)[] = []; const hasManyRelationshipFields: string[] = []; const nonModelArrayFields: string[] = []; const savedRecordName = lowerCaseFirst(modelName); diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/form-state.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/form-state.ts index 35cab0c6e..3a0cc0fc3 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/form-state.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/form-state.ts @@ -75,6 +75,8 @@ export const getRecordName = (modelName: string) => `${lowerCaseFirst(modelName) export const getLinkedDataName = (modelName: string) => `linked${capitalizeFirstLetter(modelName)}`; +export const getCanUnlinkModelName = (modelName: string) => `canUnlink${capitalizeFirstLetter(modelName)}`; + export const getCurrentValueIdentifier = (fieldName: string) => factory.createIdentifier(getCurrentValueName(fieldName)); diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts index b95419ab7..27dc3b2b1 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts @@ -22,7 +22,7 @@ import { GenericDataModel, GenericDataField, } from '@aws-amplify/codegen-ui'; -import { getRecordsName, getLinkedDataName, buildAccessChain } from './form-state'; +import { getRecordsName, getLinkedDataName, buildAccessChain, getCanUnlinkModelName } from './form-state'; import { buildBaseCollectionVariableStatement } from '../../react-studio-template-renderer-helper'; import { ImportCollection, ImportSource } from '../../imports'; import { lowerCaseFirst, getSetNameIdentifier } from '../../helpers'; @@ -1334,6 +1334,45 @@ export const buildHasManyRelationshipDataStoreStatements = ( NodeFlags.Const, ), ), + factory.createIfStatement( + factory.createBinaryExpression( + factory.createPrefixUnaryExpression( + SyntaxKind.ExclamationToken, + factory.createIdentifier(getCanUnlinkModelName(fieldName)), + ), + factory.createToken(SyntaxKind.AmpersandAmpersandToken), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(dataToUnLink), + factory.createIdentifier('length'), + ), + factory.createToken(SyntaxKind.GreaterThanToken), + factory.createNumericLiteral('0'), + ), + ), + factory.createBlock( + [ + factory.createThrowStatement( + factory.createCallExpression(factory.createIdentifier('Error'), undefined, [ + factory.createTemplateExpression(factory.createTemplateHead('', ''), [ + factory.createTemplateSpan( + factory.createPropertyAccessExpression( + factory.createIdentifier('original'), + factory.createIdentifier('id'), + ), + factory.createTemplateTail( + // eslint-disable-next-line max-len + ` cannot be unlinked from ${modelName} because ${relatedModelFields[0]} is a required field.`, + ), + ), + ]), + ]), + ), + ], + true, + ), + undefined, + ), factory.createExpressionStatement( factory.createCallExpression( // CPKProjects.forEach((r) => cPKProjectsSet.add(getIDValue.CPKProjects?.(r))); @@ -1558,120 +1597,67 @@ export const buildHasManyRelationshipDataStoreStatements = ( factory.createToken(SyntaxKind.EqualsGreaterThanToken), factory.createBlock( [ - factory.createTryStatement( - factory.createBlock( + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('promises'), + factory.createIdentifier('push'), + ), + undefined, [ - factory.createExpressionStatement( - factory.createCallExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier('promises'), - factory.createIdentifier('push'), - ), - undefined, - [ - factory.createCallExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier('DataStore'), - factory.createIdentifier('save'), - ), - undefined, - [ - factory.createCallExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier(relatedModelName), - factory.createIdentifier('copyOf'), - ), - undefined, - [ - factory.createIdentifier('original'), - factory.createArrowFunction( - undefined, - undefined, - [ - factory.createParameterDeclaration( - undefined, - undefined, - undefined, - factory.createIdentifier('updated'), - undefined, - undefined, - undefined, - ), - ], - undefined, - factory.createToken(SyntaxKind.EqualsGreaterThanToken), - factory.createBlock( - relatedModelFields.map((relatedModelField) => - factory.createExpressionStatement( - factory.createBinaryExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier('updated'), - factory.createIdentifier(relatedModelField), - ), - factory.createToken(SyntaxKind.EqualsToken), - factory.createNull(), - ), - ), - ), - true, - ), - ), - ], - ), - ], - ), - ], + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('DataStore'), + factory.createIdentifier('save'), ), - ), - ], - true, - ), - factory.createCatchClause( - factory.createVariableDeclaration( - factory.createIdentifier('err'), - undefined, - undefined, - undefined, - ), - factory.createBlock( - [ - factory.createIfStatement( - factory.createBinaryExpression( + undefined, + [ + factory.createCallExpression( factory.createPropertyAccessExpression( - factory.createIdentifier('err'), - factory.createIdentifier('message'), + factory.createIdentifier(relatedModelName), + factory.createIdentifier('copyOf'), ), - factory.createToken(SyntaxKind.EqualsEqualsEqualsToken), - factory.createStringLiteral(`Field ${relatedModelFields[0]} is required`), - ), - factory.createBlock( + undefined, [ - factory.createThrowStatement( - factory.createCallExpression(factory.createIdentifier('Error'), undefined, [ - factory.createTemplateExpression(factory.createTemplateHead('', ''), [ - factory.createTemplateSpan( - factory.createPropertyAccessExpression( - factory.createIdentifier('original'), - factory.createIdentifier('id'), - ), - factory.createTemplateTail( - // eslint-disable-next-line max-len - ` cannot be unlinked from ${modelName} because ${relatedModelFields[0]} is a required field.`, + factory.createIdentifier('original'), + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('updated'), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + relatedModelFields.map((relatedModelField) => + factory.createExpressionStatement( + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('updated'), + factory.createIdentifier(relatedModelField), + ), + factory.createToken(SyntaxKind.EqualsToken), + factory.createNull(), ), ), - ]), - ]), + ), + true, + ), ), ], - true, ), - factory.createBlock([factory.createThrowStatement(factory.createIdentifier('err'))], true), - ), - ], - true, - ), + ], + ), + ], ), - undefined, ), ], true, diff --git a/packages/codegen-ui-react/lib/forms/react-form-renderer.ts b/packages/codegen-ui-react/lib/forms/react-form-renderer.ts index e6e723317..9db24fd3d 100644 --- a/packages/codegen-ui-react/lib/forms/react-form-renderer.ts +++ b/packages/codegen-ui-react/lib/forms/react-form-renderer.ts @@ -48,7 +48,7 @@ import { SyntaxKind, TypeAliasDeclaration, } from 'typescript'; -import { buildUseStateExpression, lowerCaseFirst } from '../helpers'; +import { buildInitConstVariableExpression, buildUseStateExpression, lowerCaseFirst } from '../helpers'; import { ImportCollection, ImportSource, ImportValue } from '../imports'; import { PrimitiveTypeParameter, Primitive, primitiveOverrideProp } from '../primitive'; import { getComponentPropName } from '../react-component-render-helper'; @@ -86,6 +86,7 @@ import { getInitialValues, getUseStateHooks, resetStateFunction, + getCanUnlinkModelName, } from './form-renderer-helper/form-state'; import { isModelDataType, shouldWrapInArrayField } from './form-renderer-helper/render-checkers'; import { @@ -484,6 +485,12 @@ export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer< const linkedDataName = getLinkedDataName(fieldName); linkedDataNames.push(linkedDataName); statements.push(buildUseStateExpression(linkedDataName, factory.createIdentifier('[]'))); + statements.push( + buildInitConstVariableExpression( + getCanUnlinkModelName(fieldName), + value.relationship.canUnlinkAssociatedModel ? factory.createTrue() : factory.createFalse(), + ), + ); } if (value.relationship.type === 'BELONGS_TO' || value.relationship?.type === 'HAS_ONE') { linkedDataNames.push(fieldName); diff --git a/packages/codegen-ui-react/lib/helpers/index.ts b/packages/codegen-ui-react/lib/helpers/index.ts index 5a9266f3f..648ea4e69 100644 --- a/packages/codegen-ui-react/lib/helpers/index.ts +++ b/packages/codegen-ui-react/lib/helpers/index.ts @@ -75,6 +75,24 @@ export const buildUseStateExpression = (name: string, defaultValue: Expression): ); }; +/** + * Create statement to declare and initialized a const. + * + * const name = value; + * @param name + * @param value + * @returns + */ +export const buildInitConstVariableExpression = (name: string, value: Expression): Statement => { + return factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, value)], + NodeFlags.Const, + ), + ); +}; + export function fieldNeedsRelationshipLoadedForCollection(field: GenericDataField, dataSchema: GenericDataSchema) { const { relationship, dataType } = field; if (!relationship || !dataType) { diff --git a/packages/codegen-ui/lib/__tests__/generic-from-datastore.test.ts b/packages/codegen-ui/lib/__tests__/generic-from-datastore.test.ts index b0e9b79a6..22448ae0b 100644 --- a/packages/codegen-ui/lib/__tests__/generic-from-datastore.test.ts +++ b/packages/codegen-ui/lib/__tests__/generic-from-datastore.test.ts @@ -74,6 +74,7 @@ describe('getGenericFromDataStore', () => { type: 'HAS_MANY', relatedModelName: 'Teacher', relatedModelFields: ['student'], + canUnlinkAssociatedModel: false, relatedJoinFieldName: 'teacher', relatedJoinTableName: 'StudentTeacher', }); @@ -82,6 +83,7 @@ describe('getGenericFromDataStore', () => { type: 'HAS_MANY', relatedModelName: 'Student', relatedModelFields: ['teacher'], + canUnlinkAssociatedModel: false, relatedJoinFieldName: 'student', relatedJoinTableName: 'StudentTeacher', }); @@ -107,6 +109,7 @@ describe('getGenericFromDataStore', () => { type: 'HAS_MANY', relatedModelName: 'Dog', relatedModelFields: ['ownerID'], + canUnlinkAssociatedModel: true, relatedJoinFieldName: undefined, relatedJoinTableName: undefined, }); @@ -135,6 +138,7 @@ describe('getGenericFromDataStore', () => { type: 'HAS_MANY', relatedModelName: 'Teacher', relatedModelFields: ['student'], + canUnlinkAssociatedModel: false, relatedJoinFieldName: 'teacher', relatedJoinTableName: 'StudentTeacher', }); @@ -143,6 +147,7 @@ describe('getGenericFromDataStore', () => { type: 'HAS_MANY', relatedModelName: 'Student', relatedModelFields: ['teacher'], + canUnlinkAssociatedModel: false, relatedJoinFieldName: 'student', relatedJoinTableName: 'StudentTeacher', }); @@ -168,6 +173,7 @@ describe('getGenericFromDataStore', () => { type: 'HAS_MANY', relatedModelName: 'Dog', relatedModelFields: ['ownerID'], + canUnlinkAssociatedModel: true, relatedJoinFieldName: undefined, relatedJoinTableName: undefined, }); @@ -201,18 +207,20 @@ describe('getGenericFromDataStore', () => { const genericSchema = getGenericFromDataStore(schemaWithAssumptions); const userFields = genericSchema.models.User.fields; - expect(userFields.friends.relationship).toStrictEqual({ + expect(userFields.friends.relationship).toStrictEqual({ type: 'HAS_MANY', relatedModelName: 'Friend', relatedModelFields: ['friendId'], + canUnlinkAssociatedModel: true, relatedJoinFieldName: undefined, relatedJoinTableName: undefined, }); - expect(userFields.posts.relationship).toStrictEqual({ + expect(userFields.posts.relationship).toStrictEqual({ type: 'HAS_MANY', relatedModelName: 'Post', relatedModelFields: ['userPostsId'], + canUnlinkAssociatedModel: true, relatedJoinFieldName: undefined, relatedJoinTableName: undefined, }); diff --git a/packages/codegen-ui/lib/generic-from-datastore.ts b/packages/codegen-ui/lib/generic-from-datastore.ts index 439ee904e..ae780bc9c 100644 --- a/packages/codegen-ui/lib/generic-from-datastore.ts +++ b/packages/codegen-ui/lib/generic-from-datastore.ts @@ -100,9 +100,14 @@ export function getGenericFromDataStore(dataStoreSchema: DataStoreSchema): Gener const associatedFieldNames = Array.isArray(field.association?.associatedWith) ? field.association.associatedWith : [field.association.associatedWith]; + let canUnlinkAssociatedModel = true; associatedFieldNames.forEach((associatedFieldName) => { const associatedField = associatedModel?.fields[associatedFieldName]; + // if any of the associatedField is required, you cannot unlink from parent model + if (associatedField?.isRequired) { + canUnlinkAssociatedModel = false; + } // if the associated model is a join table, update relatedModelName to the actual related model if ( associatedField && @@ -133,6 +138,7 @@ export function getGenericFromDataStore(dataStoreSchema: DataStoreSchema): Gener }); modelRelationship = { type: relationshipType, + canUnlinkAssociatedModel, relatedModelName, relatedModelFields: associatedFieldNames, relatedJoinFieldName, diff --git a/packages/codegen-ui/lib/types/data.ts b/packages/codegen-ui/lib/types/data.ts index 5b2502edc..4137e1dc5 100644 --- a/packages/codegen-ui/lib/types/data.ts +++ b/packages/codegen-ui/lib/types/data.ts @@ -37,6 +37,7 @@ export type CommonRelationshipType = { export type HasManyRelationshipType = { type: 'HAS_MANY'; relatedModelFields: string[]; + canUnlinkAssociatedModel: boolean; relatedJoinFieldName?: string; relatedJoinTableName?: string; } & CommonRelationshipType; From 506fbb6af5b05ad3415525a9ce43664724ca8b48 Mon Sep 17 00:00:00 2001 From: charles shin Date: Thu, 19 Jan 2023 10:02:23 -0800 Subject: [PATCH 2/3] fix: edit error message for updating hasMany parent --- ...studio-ui-codegen-react-forms.test.ts.snap | 30 +++---- .../form-renderer-helper/relationship.ts | 78 +++++++++---------- 2 files changed, 54 insertions(+), 54 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 7db6bb8a8..1e4df2a76 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 @@ -3750,11 +3750,6 @@ export default function UpdateCompositeDogForm(props) { const compositeToysToUnLink = []; const compositeToysSet = new Set(); const linkedCompositeToysSet = new Set(); - if (!canUnlinkCompositeToys && compositeToysToUnLink.length > 0) { - throw Error( - \`\${original.id} cannot be unlinked from CompositeDog because compositeDogCompositeToysName is a required field.\` - ); - } CompositeToys.forEach((r) => compositeToysSet.add(getIDValue.CompositeToys?.(r)) ); @@ -3772,6 +3767,11 @@ export default function UpdateCompositeDogForm(props) { } }); compositeToysToUnLink.forEach((original) => { + if (!canUnlinkCompositeToys && compositeToysToUnLink.length > 0) { + throw Error( + \`CompositeToy \${original.id} cannot be unlinked from CompositeDog because compositeDogCompositeToysName is a required field.\` + ); + } promises.push( DataStore.save( CompositeToy.copyOf(original, (updated) => { @@ -4825,11 +4825,6 @@ export default function UpdateCPKTeacherForm(props) { const cPKProjectsToUnLink = []; const cPKProjectsSet = new Set(); const linkedCPKProjectsSet = new Set(); - if (!canUnlinkCPKProjects && cPKProjectsToUnLink.length > 0) { - throw Error( - \`\${original.id} cannot be unlinked from CPKTeacher because cPKTeacherID is a required field.\` - ); - } CPKProjects.forEach((r) => cPKProjectsSet.add(getIDValue.CPKProjects?.(r)) ); @@ -4847,6 +4842,11 @@ export default function UpdateCPKTeacherForm(props) { } }); cPKProjectsToUnLink.forEach((original) => { + if (!canUnlinkCPKProjects && cPKProjectsToUnLink.length > 0) { + throw Error( + \`CPKProject \${original.id} cannot be unlinked from CPKTeacher because cPKTeacherID is a required field.\` + ); + } promises.push( DataStore.save( CPKProject.copyOf(original, (updated) => { @@ -11442,11 +11442,6 @@ export default function SchoolUpdateForm(props) { const studentsToUnLink = []; const studentsSet = new Set(); const linkedStudentsSet = new Set(); - if (!canUnlinkStudents && studentsToUnLink.length > 0) { - throw Error( - \`\${original.id} cannot be unlinked from School because schoolID is a required field.\` - ); - } Students.forEach((r) => studentsSet.add(getIDValue.Students?.(r))); linkedStudents.forEach((r) => linkedStudentsSet.add(getIDValue.Students?.(r)) @@ -11462,6 +11457,11 @@ export default function SchoolUpdateForm(props) { } }); studentsToUnLink.forEach((original) => { + if (!canUnlinkStudents && studentsToUnLink.length > 0) { + throw Error( + \`Student \${original.id} cannot be unlinked from School because schoolID is a required field.\` + ); + } promises.push( DataStore.save( Student.copyOf(original, (updated) => { diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts index 27dc3b2b1..65ebd4639 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts @@ -1334,45 +1334,6 @@ export const buildHasManyRelationshipDataStoreStatements = ( NodeFlags.Const, ), ), - factory.createIfStatement( - factory.createBinaryExpression( - factory.createPrefixUnaryExpression( - SyntaxKind.ExclamationToken, - factory.createIdentifier(getCanUnlinkModelName(fieldName)), - ), - factory.createToken(SyntaxKind.AmpersandAmpersandToken), - factory.createBinaryExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier(dataToUnLink), - factory.createIdentifier('length'), - ), - factory.createToken(SyntaxKind.GreaterThanToken), - factory.createNumericLiteral('0'), - ), - ), - factory.createBlock( - [ - factory.createThrowStatement( - factory.createCallExpression(factory.createIdentifier('Error'), undefined, [ - factory.createTemplateExpression(factory.createTemplateHead('', ''), [ - factory.createTemplateSpan( - factory.createPropertyAccessExpression( - factory.createIdentifier('original'), - factory.createIdentifier('id'), - ), - factory.createTemplateTail( - // eslint-disable-next-line max-len - ` cannot be unlinked from ${modelName} because ${relatedModelFields[0]} is a required field.`, - ), - ), - ]), - ]), - ), - ], - true, - ), - undefined, - ), factory.createExpressionStatement( factory.createCallExpression( // CPKProjects.forEach((r) => cPKProjectsSet.add(getIDValue.CPKProjects?.(r))); @@ -1597,6 +1558,45 @@ export const buildHasManyRelationshipDataStoreStatements = ( factory.createToken(SyntaxKind.EqualsGreaterThanToken), factory.createBlock( [ + factory.createIfStatement( + factory.createBinaryExpression( + factory.createPrefixUnaryExpression( + SyntaxKind.ExclamationToken, + factory.createIdentifier(getCanUnlinkModelName(fieldName)), + ), + factory.createToken(SyntaxKind.AmpersandAmpersandToken), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(dataToUnLink), + factory.createIdentifier('length'), + ), + factory.createToken(SyntaxKind.GreaterThanToken), + factory.createNumericLiteral('0'), + ), + ), + factory.createBlock( + [ + factory.createThrowStatement( + factory.createCallExpression(factory.createIdentifier('Error'), undefined, [ + factory.createTemplateExpression(factory.createTemplateHead(`${relatedModelName} `), [ + factory.createTemplateSpan( + factory.createPropertyAccessExpression( + factory.createIdentifier('original'), + factory.createIdentifier('id'), + ), + factory.createTemplateTail( + // eslint-disable-next-line max-len + ` cannot be unlinked from ${modelName} because ${relatedModelFields[0]} is a required field.`, + ), + ), + ]), + ]), + ), + ], + true, + ), + undefined, + ), factory.createExpressionStatement( factory.createCallExpression( factory.createPropertyAccessExpression( From c3c95997cc824e3cf2ae561fa6e191a2986976b9 Mon Sep 17 00:00:00 2001 From: charles shin Date: Thu, 19 Jan 2023 10:30:55 -0800 Subject: [PATCH 3/3] fix: extract primary key from model def --- .../SchoolUpdateForm.jsx | 5 +++++ ...studio-ui-codegen-react-forms.test.ts.snap | 10 +++++----- .../form-renderer-helper/relationship.ts | 19 ++++--------------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/codegen-ui-golden-files/lib/react/forms/datastore-update-form-with-has-many-relationship/SchoolUpdateForm.jsx b/packages/codegen-ui-golden-files/lib/react/forms/datastore-update-form-with-has-many-relationship/SchoolUpdateForm.jsx index 774148a8f..39f476c9b 100644 --- a/packages/codegen-ui-golden-files/lib/react/forms/datastore-update-form-with-has-many-relationship/SchoolUpdateForm.jsx +++ b/packages/codegen-ui-golden-files/lib/react/forms/datastore-update-form-with-has-many-relationship/SchoolUpdateForm.jsx @@ -273,6 +273,11 @@ export default function SchoolUpdateForm(props) { const promises = []; studentsToUnLink.forEach((original) => { + if (!canUnlinkStudents) { + throw Error( + `Student ${original.id} cannot be unlinked from School because schoolID is a required field.`, + ); + } promises.push( DataStore.save( Student.copyOf(original, (updated) => { 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 1e4df2a76..e31d3affe 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 @@ -3767,9 +3767,9 @@ export default function UpdateCompositeDogForm(props) { } }); compositeToysToUnLink.forEach((original) => { - if (!canUnlinkCompositeToys && compositeToysToUnLink.length > 0) { + if (!canUnlinkCompositeToys) { throw Error( - \`CompositeToy \${original.id} cannot be unlinked from CompositeDog because compositeDogCompositeToysName is a required field.\` + \`CompositeToy \${original.kind} cannot be unlinked from CompositeDog because compositeDogCompositeToysName is a required field.\` ); } promises.push( @@ -4842,9 +4842,9 @@ export default function UpdateCPKTeacherForm(props) { } }); cPKProjectsToUnLink.forEach((original) => { - if (!canUnlinkCPKProjects && cPKProjectsToUnLink.length > 0) { + if (!canUnlinkCPKProjects) { throw Error( - \`CPKProject \${original.id} cannot be unlinked from CPKTeacher because cPKTeacherID is a required field.\` + \`CPKProject \${original.specialProjectId} cannot be unlinked from CPKTeacher because cPKTeacherID is a required field.\` ); } promises.push( @@ -11457,7 +11457,7 @@ export default function SchoolUpdateForm(props) { } }); studentsToUnLink.forEach((original) => { - if (!canUnlinkStudents && studentsToUnLink.length > 0) { + if (!canUnlinkStudents) { throw Error( \`Student \${original.id} cannot be unlinked from School because schoolID is a required field.\` ); diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts index 65ebd4639..526f63a5b 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts @@ -1559,20 +1559,9 @@ export const buildHasManyRelationshipDataStoreStatements = ( factory.createBlock( [ factory.createIfStatement( - factory.createBinaryExpression( - factory.createPrefixUnaryExpression( - SyntaxKind.ExclamationToken, - factory.createIdentifier(getCanUnlinkModelName(fieldName)), - ), - factory.createToken(SyntaxKind.AmpersandAmpersandToken), - factory.createBinaryExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier(dataToUnLink), - factory.createIdentifier('length'), - ), - factory.createToken(SyntaxKind.GreaterThanToken), - factory.createNumericLiteral('0'), - ), + factory.createPrefixUnaryExpression( + SyntaxKind.ExclamationToken, + factory.createIdentifier(getCanUnlinkModelName(fieldName)), ), factory.createBlock( [ @@ -1582,7 +1571,7 @@ export const buildHasManyRelationshipDataStoreStatements = ( factory.createTemplateSpan( factory.createPropertyAccessExpression( factory.createIdentifier('original'), - factory.createIdentifier('id'), + factory.createIdentifier(keys[0]), ), factory.createTemplateTail( // eslint-disable-next-line max-len