diff --git a/packages/codegen-ui-react/package.json b/packages/codegen-ui-react/package.json index 75c664c5..e2cb1c7b 100644 --- a/packages/codegen-ui-react/package.json +++ b/packages/codegen-ui-react/package.json @@ -14,7 +14,7 @@ "scripts": { "test": "jest", "test:watch": "jest --watch", - "test:ci": "jest --ci --maxWorkers=30%", + "test:ci": "jest --ci -i", "test:update": "jest --updateSnapshot", "build": "tsc -p tsconfig.build.json", "build:watch": "npm run build -- --watch" diff --git a/packages/codegen-ui/lib/__tests__/__utils__/introspection-schemas/schema-with-2-has-one.json b/packages/codegen-ui/lib/__tests__/__utils__/introspection-schemas/schema-with-2-has-one.json new file mode 100644 index 00000000..ffda7c13 --- /dev/null +++ b/packages/codegen-ui/lib/__tests__/__utils__/introspection-schemas/schema-with-2-has-one.json @@ -0,0 +1,226 @@ +{ + "version": "1", + "models": { + "Foo": { + "name": "Foo", + "fields": { + "id": { + "name": "id", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "User1": { + "name": "User1", + "isArray": false, + "type": { + "model": "User" + }, + "isRequired": false, + "attributes": [], + "association": { + "connectionType": "HAS_ONE", + "associatedWith": [ + "id" + ], + "targetNames": [ + "fooUser1Id" + ] + } + }, + "User2": { + "name": "User2", + "isArray": false, + "type": { + "model": "User" + }, + "isRequired": false, + "attributes": [], + "association": { + "connectionType": "HAS_ONE", + "associatedWith": [ + "id" + ], + "targetNames": [ + "fooUser2Id" + ] + } + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "fooUser1Id": { + "name": "fooUser1Id", + "isArray": false, + "type": "ID", + "isRequired": false, + "attributes": [] + }, + "fooUser2Id": { + "name": "fooUser2Id", + "isArray": false, + "type": "ID", + "isRequired": false, + "attributes": [] + } + }, + "syncable": true, + "pluralName": "Foos", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "auth", + "properties": { + "rules": [ + { + "allow": "public", + "operations": [ + "create", + "update", + "delete", + "read" + ] + } + ] + } + } + ], + "primaryKeyInfo": { + "isCustomPrimaryKey": false, + "primaryKeyFieldName": "id", + "sortKeyFieldNames": [] + } + }, + "User": { + "name": "User", + "fields": { + "id": { + "name": "id", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "name": { + "name": "name", + "isArray": false, + "type": "String", + "isRequired": true, + "attributes": [] + }, + "image": { + "name": "image", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "bio": { + "name": "bio", + "isArray": false, + "type": "String", + "isRequired": true, + "attributes": [] + }, + "gender": { + "name": "gender", + "isArray": false, + "type": { + "enum": "Genders" + }, + "isRequired": true, + "attributes": [] + }, + "lookingFor": { + "name": "lookingFor", + "isArray": false, + "type": { + "enum": "Genders" + }, + "isRequired": true, + "attributes": [] + }, + "sub": { + "name": "sub", + "isArray": false, + "type": "String", + "isRequired": true, + "attributes": [] + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + } + }, + "syncable": true, + "pluralName": "Users", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "auth", + "properties": { + "rules": [ + { + "allow": "public", + "operations": [ + "create", + "update", + "delete", + "read" + ] + } + ] + } + } + ], + "primaryKeyInfo": { + "isCustomPrimaryKey": false, + "primaryKeyFieldName": "id", + "sortKeyFieldNames": [] + } + } + }, + "enums": { + "Genders": { + "name": "Genders", + "values": [ + "MALE", + "FEMALE", + "OTHER" + ] + } + }, + "nonModels": {} +} diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts index 286e8d98..362bbd22 100644 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts +++ b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts @@ -14,13 +14,16 @@ limitations under the License. */ +import { ModelIntrospectionSchema } from '@aws-amplify/appsync-modelgen-plugin'; import { mapModelFieldsConfigs, getFieldTypeMapKey, getFieldConfigFromModelField, } from '../../../generate-form-definition/helpers'; +import { getGenericFromDataStore } from '../../../generic-from-datastore'; import { FormDefinition, GenericDataField, GenericDataSchema } from '../../../types'; import { getBasicFormDefinition } from '../../__utils__/basic-form-definition'; +import schemaWith2HasOne from '../../__utils__/introspection-schemas/schema-with-2-has-one.json'; describe('mapModelFieldsConfigs', () => { it('should map to elementMatrix and add to modelFieldsConfigs', () => { @@ -100,6 +103,74 @@ describe('mapModelFieldsConfigs', () => { expect(() => mapModelFieldsConfigs({ dataTypeName: 'Cat', formDefinition, dataSchema })).toThrow(); }); + it('should properly map all hasOne relationships if multiple are provided', () => { + const genericDataSchema = getGenericFromDataStore(schemaWith2HasOne as unknown as ModelIntrospectionSchema); + + const modelFields = mapModelFieldsConfigs({ + dataTypeName: 'Foo', + formDefinition: { + form: { + layoutStyle: { horizontalGap: { value: '12' }, verticalGap: { value: '12' }, outerPadding: { value: '5' } }, + labelDecorator: 'none', + }, + elements: {}, + buttons: { + buttonConfigs: {}, + position: '', + buttonMatrix: [], + }, + elementMatrix: [], + }, + dataSchema: genericDataSchema, + formActionType: 'create', + featureFlags: { isRelationshipSupported: true }, + }); + + const userValueMappings = { + bindingProperties: { + User: { + bindingProperties: { + model: 'User', + }, + type: 'Data', + }, + }, + values: [ + { + displayValue: { + concat: [ + { + bindingProperties: { + field: 'name', + property: 'User', + }, + }, + { + value: ' - ', + }, + { + bindingProperties: { + field: 'id', + property: 'User', + }, + }, + ], + isDefault: true, + }, + value: { + bindingProperties: { + field: 'id', + property: 'User', + }, + }, + }, + ], + }; + + expect(modelFields.fooUser1Id.inputType?.valueMappings).toEqual(userValueMappings); + expect(modelFields.fooUser2Id.inputType?.valueMappings).toEqual(userValueMappings); + }); + it('should generate config from id field but not add it to matrix', () => { const formDefinition: FormDefinition = getBasicFormDefinition(); diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts index 7d0cc534..7d2dc754 100644 --- a/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts +++ b/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts @@ -24,7 +24,9 @@ import { FormFeatureFlags, GenericDataField, GenericDataModel, + GenericDataRelationshipType, GenericDataSchema, + HasManyRelationshipType, ModelFieldsConfigs, StudioFieldInputConfig, StudioForm, @@ -37,6 +39,9 @@ import { FIELD_TYPE_MAP } from './field-type-map'; const isModelDataType = (field: GenericDataField): field is GenericDataField & { dataType: { model: string } } => typeof field.dataType === 'object' && 'model' in field.dataType; +const isHasManyRelationship = (relationship: GenericDataRelationshipType): relationship is HasManyRelationshipType => + ('isHasManyIndex' in relationship && relationship.isHasManyIndex) ?? false; + function extractCorrespondingKey({ thisModel, relatedModel, @@ -46,57 +51,57 @@ function extractCorrespondingKey({ relatedModel: GenericDataModel; relationshipFieldName: string; }): string { - const relationshipField = thisModel.fields[relationshipFieldName]; - - if ( - relationshipField.relationship && - 'isHasManyIndex' in relationshipField.relationship && - relationshipField.relationship.isHasManyIndex - ) { - const correspondingFieldTuple = Object.entries(relatedModel.fields).find( - ([, field]) => - field.relationship?.type === 'HAS_MANY' && - field.relationship?.relatedModelFields.includes(relationshipFieldName), - ); - if (correspondingFieldTuple) { - const correspondingField = correspondingFieldTuple[1].relationship; - if (correspondingField?.type === 'HAS_MANY') { - const indexOfKey = correspondingField.relatedModelFields.indexOf(relationshipFieldName); - if (indexOfKey !== -1) { - const relatedPrimaryKey = relatedModel.primaryKeys[indexOfKey]; - if (relatedPrimaryKey) { - // secondary index on child of 1:m - return relatedPrimaryKey; + const { relationship: thisModelRelationship } = thisModel.fields[relationshipFieldName] ?? {}; + + if (thisModelRelationship) { + if (isHasManyRelationship(thisModelRelationship)) { + const correspondingFieldTuple = Object.values(relatedModel.fields).find( + ({ relationship }) => + relationship?.type === 'HAS_MANY' && relationship?.relatedModelFields.includes(relationshipFieldName), + ); + + if (correspondingFieldTuple) { + const correspondingField = correspondingFieldTuple.relationship; + + if (correspondingField?.type === 'HAS_MANY') { + const indexOfKey = correspondingField.relatedModelFields.indexOf(relationshipFieldName); + + if (indexOfKey !== -1) { + const relatedPrimaryKey = relatedModel.primaryKeys[indexOfKey]; + + if (relatedPrimaryKey) { + // secondary index on child of 1:m + return relatedPrimaryKey; + } } } } - } - } - - if ( - relationshipField.relationship && - (relationshipField.relationship.type === 'HAS_ONE' || relationshipField.relationship.type === 'BELONGS_TO') - ) { - const modelRelationshipFieldTuple = Object.entries(thisModel.fields).find( - ([, field]) => - field.relationship?.type === relationshipField.relationship?.type && - isModelDataType(field) && - field.relationship?.relatedModelName === relationshipField.relationship?.relatedModelName, - ); + } else { + const modelRelationshipFieldTuple = Object.values(thisModel.fields) + .filter(isModelDataType) + .find(({ relationship: matchingFieldRelationship }) => { + return ( + matchingFieldRelationship?.type === thisModelRelationship.type && + matchingFieldRelationship?.relatedModelName === thisModelRelationship.relatedModelName && + matchingFieldRelationship?.associatedFields?.includes(relationshipFieldName) + ); + }); - if (modelRelationshipFieldTuple) { - const modelRelationshipField = modelRelationshipFieldTuple[1].relationship; - if ( - modelRelationshipField && - 'associatedFields' in modelRelationshipField && - modelRelationshipField.associatedFields - ) { - const indexOfKey = modelRelationshipField.associatedFields.indexOf(relationshipFieldName); - if (indexOfKey !== -1) { - const relatedPrimaryKey = relatedModel.primaryKeys[indexOfKey]; - if (relatedPrimaryKey) { - // index on parent of 1:1 - return relatedPrimaryKey; + if (modelRelationshipFieldTuple) { + const modelRelationshipField = modelRelationshipFieldTuple.relationship; + + if ( + modelRelationshipField && + 'associatedFields' in modelRelationshipField && + modelRelationshipField.associatedFields + ) { + const indexOfKey = modelRelationshipField.associatedFields.indexOf(relationshipFieldName); + if (indexOfKey !== -1) { + const relatedPrimaryKey = relatedModel.primaryKeys[indexOfKey]; + if (relatedPrimaryKey) { + // index on parent of 1:1 + return relatedPrimaryKey; + } } } } diff --git a/packages/codegen-ui/package.json b/packages/codegen-ui/package.json index f34958ae..4d58b075 100644 --- a/packages/codegen-ui/package.json +++ b/packages/codegen-ui/package.json @@ -13,7 +13,7 @@ ], "scripts": { "test": "jest", - "test:ci": "jest --ci --maxWorkers=30%", + "test:ci": "jest -i --ci", "test:update": "jest --updateSnapshot", "build": "tsc -p tsconfig.build.json", "build:watch": "npm run build -- --watch"