Skip to content

Commit

Permalink
feat: add graphql support for relationship forms with autocomplete field
Browse files Browse the repository at this point in the history
  • Loading branch information
bombguy authored and awinberg-aws committed Jul 6, 2023
1 parent 6dd8498 commit 6c4742b
Show file tree
Hide file tree
Showing 18 changed files with 2,737 additions and 480 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -752,8 +752,8 @@ describe('amplify form renderer tests', () => {
{ isNonModelSupported: true, isRelationshipSupported: true },
);

// check binding call is generated
expect(componentText).toContain(').data.listAuthors.items;');
// check for import statement for graphql operation
expect(componentText).not.toContain('DataStore');

expect(componentText).toMatchSnapshot();
expect(declaration).toMatchSnapshot();
Expand All @@ -772,7 +772,21 @@ describe('amplify form renderer tests', () => {

expect(componentText).toContain('await API.graphql({');
expect(componentText).toContain('query: updateComment');
expect(componentText).toContain(').data.listPosts.items');

expect(componentText).toMatchSnapshot();
expect(declaration).toMatchSnapshot();
});

it('should generate a relationship update form with autocomplete', () => {
const { componentText, declaration } = generateWithAmplifyFormRenderer(
'forms/relationships/update-post',
'datastore/relationships/has-many-autocomplete-post',
{ ...defaultCLIRenderConfig, ...rendererConfigWithGraphQL },
{ isNonModelSupported: true, isRelationshipSupported: true },
);

// check for import statement for graphql operation
expect(componentText).not.toContain('DataStore');

expect(componentText).toMatchSnapshot();
expect(declaration).toMatchSnapshot();
Expand Down Expand Up @@ -838,9 +852,8 @@ describe('amplify form renderer tests', () => {
{ isNonModelSupported: true, isRelationshipSupported: true },
);

// check binding calls are generated
expect(componentText).toContain(').data.listAuthors.items;');
expect(componentText).toContain(').data.listTitles.items;');
// check for import statement for graphql operation
expect(componentText).not.toContain('DataStore');

expect(componentText).toMatchSnapshot();
expect(declaration).toMatchSnapshot();
Expand All @@ -854,8 +867,8 @@ describe('amplify form renderer tests', () => {
{ isNonModelSupported: true, isRelationshipSupported: true },
);

// check binding call is generated
expect(componentText).toContain(').data.listTeams.items;');
// check for import statement for graphql operation
expect(componentText).not.toContain('DataStore');

expect(componentText).toMatchSnapshot();
expect(declaration).toMatchSnapshot();
Expand All @@ -869,8 +882,8 @@ describe('amplify form renderer tests', () => {
{ isNonModelSupported: true, isRelationshipSupported: true },
);

// check binding call is generated
expect(componentText).toContain(').data.listPosts.items;');
// check for import statement for graphql operation
expect(componentText).not.toContain('DataStore');

// check custom display value is set
expect(componentText).toContain('Posts: (r) => r?.title');
Expand All @@ -887,8 +900,8 @@ describe('amplify form renderer tests', () => {
{ isNonModelSupported: true, isRelationshipSupported: true },
);

// check binding call is generated
expect(componentText).toContain(').data.listStudents.items;');
// check for import statement for graphql operation
expect(componentText).not.toContain('DataStore');

// check custom display value is set
expect(componentText).toContain('Students: (r) => r?.name');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ import { getAutocompleteOptionsProp } from './model-values';
import { buildCtaLayoutProperties } from '../../react-component-render-helper';
import { getModelNameProp, lowerCaseFirst } from '../../helpers';
import { COMPOSITE_PRIMARY_KEY_PROP_NAME } from '../../utils/constants';
import { DataApiKind } from '../../react-render-config';

export const addFormAttributes = (
component: StudioComponent | StudioComponentChild,
formMetadata: FormMetadata,
dataSchema?: GenericDataSchema,
dataApi?: DataApiKind,
) => {
const { name: componentName, componentType } = component;
const {
Expand Down Expand Up @@ -93,7 +95,15 @@ export const addFormAttributes = (
}

if (fieldConfig.componentType === 'Autocomplete') {
attributes.push(getAutocompleteOptionsProp({ fieldName: componentName, fieldConfig }));
attributes.push(getAutocompleteOptionsProp({ fieldName: renderedVariableName, fieldConfig, dataApi }));
if (fieldConfig.relationship && dataApi === 'GraphQL') {
attributes.push(
factory.createJsxAttribute(
factory.createIdentifier('isLoading'),
factory.createJsxExpression(undefined, factory.createIdentifier(`${renderedVariableName}Loading`)),
),
);
}
attributes.push(
buildOnSelect({ sanitizedFieldName: renderedVariableName, fieldConfig, fieldName: componentName }),
);
Expand All @@ -103,7 +113,7 @@ export const addFormAttributes = (
if (formMetadata.formActionType === 'update' && !fieldConfig.isArray && !isControlledComponent(componentType)) {
attributes.push(renderDefaultValueAttribute(renderedVariableName, fieldConfig, componentType));
}
attributes.push(buildOnChangeStatement(component, formMetadata.fieldConfigs));
attributes.push(buildOnChangeStatement(component, formMetadata.fieldConfigs, dataApi));
attributes.push(buildOnBlurStatement(componentName, fieldConfig));
attributes.push(
factory.createJsxAttribute(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ function unlinkModelRecordExpression({
factory.createCallExpression(
factory.createPropertyAccessExpression(factory.createIdentifier('promises'), factory.createIdentifier('push')),
undefined,
[getGraphqlCallExpression(ActionType.UPDATE, modelName, importCollection, inputs)],
[getGraphqlCallExpression(ActionType.UPDATE, modelName, importCollection, { inputs })],
),
);
}
Expand Down Expand Up @@ -278,7 +278,7 @@ function linkModelRecordExpression({
factory.createCallExpression(
factory.createPropertyAccessExpression(factory.createIdentifier('promises'), factory.createIdentifier('push')),
undefined,
[getGraphqlCallExpression(ActionType.UPDATE, importedRelatedModelName, importCollection, inputs)],
[getGraphqlCallExpression(ActionType.UPDATE, importedRelatedModelName, importCollection, { inputs })],
),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ import { ImportCollection } from '../../imports';
import { getBiDirectionalRelationshipStatements } from './bidirectional-relationship';
import { generateModelObjectToSave } from './parse-fields';
import { DataApiKind } from '../../react-render-config';
import { ActionType, getGraphqlCallExpression } from '../../utils/graphql';
import {
ActionType,
getGraphqlCallExpression,
getGraphqlQueryForModel,
wrapInParenthesizedExpression,
} from '../../utils/graphql';

const getRecordCreateCallExpression = ({
savedObjectName,
Expand All @@ -52,7 +57,7 @@ const getRecordCreateCallExpression = ({
if (dataApi === 'GraphQL') {
const inputs = [factory.createSpreadAssignment(factory.createIdentifier(savedObjectName))];

return getGraphqlCallExpression(ActionType.CREATE, importedModelName, importCollection, inputs);
return getGraphqlCallExpression(ActionType.CREATE, importedModelName, importCollection, { inputs });
}

return factory.createCallExpression(
Expand All @@ -68,13 +73,17 @@ const getRecordCreateCallExpression = ({

const getRecordUpdateDataStoreCallExpression = ({
savedObjectName,
savedRecordName,
thisModelPrimaryKeys,
modelName,
importedModelName,
fieldConfigs,
importCollection,
dataApi,
}: {
savedObjectName: string;
savedRecordName: string;
thisModelPrimaryKeys: string[];
modelName: string;
importedModelName: string;
fieldConfigs: Record<string, FieldConfigMetadata>;
Expand All @@ -91,9 +100,17 @@ const getRecordUpdateDataStoreCallExpression = ({
});

if (dataApi === 'GraphQL') {
const inputs = [factory.createSpreadAssignment(factory.createIdentifier(savedObjectName))];
const inputs = [
...thisModelPrimaryKeys.map((key) =>
factory.createPropertyAssignment(
factory.createIdentifier(key),
factory.createPropertyAccessExpression(factory.createIdentifier(`${savedRecordName}Record`), key),
),
),
factory.createSpreadAssignment(factory.createIdentifier(savedObjectName)),
];

return getGraphqlCallExpression(ActionType.UPDATE, importedModelName, importCollection, inputs);
return getGraphqlCallExpression(ActionType.UPDATE, importedModelName, importCollection, { inputs });
}

return factory.createCallExpression(
Expand Down Expand Up @@ -355,6 +372,8 @@ export const buildExpression = (
if (dataStoreActionType === 'update') {
const recordUpdateDataStoreCallExpression = getRecordUpdateDataStoreCallExpression({
savedObjectName,
savedRecordName,
thisModelPrimaryKeys,
modelName,
importedModelName,
fieldConfigs,
Expand Down Expand Up @@ -665,16 +684,23 @@ export const buildUpdateDatastoreQuery = (

const queryCall =
dataApi === 'GraphQL'
? getGraphqlCallExpression(ActionType.GET, importedModelName, importCollection, [
factory.createPropertyAssignment(factory.createIdentifier('id'), factory.createIdentifier('idProp')),
])
: factory.createCallExpression(
factory.createPropertyAccessExpression(
factory.createIdentifier('DataStore'),
factory.createIdentifier('query'),
? wrapInParenthesizedExpression(
getGraphqlCallExpression(ActionType.GET, importedModelName, importCollection, {
inputs: [
factory.createPropertyAssignment(factory.createIdentifier('id'), factory.createIdentifier('idProp')),
],
}),
['data', getGraphqlQueryForModel(ActionType.GET, importedModelName)],
)
: factory.createAwaitExpression(
factory.createCallExpression(
factory.createPropertyAccessExpression(
factory.createIdentifier('DataStore'),
factory.createIdentifier('query'),
),
undefined,
[factory.createIdentifier(importedModelName), pkQueryIdentifier],
),
undefined,
[factory.createIdentifier(importedModelName), pkQueryIdentifier],
);

return [
Expand Down Expand Up @@ -705,7 +731,7 @@ export const buildUpdateDatastoreQuery = (
factory.createConditionalExpression(
pkQueryIdentifier,
factory.createToken(SyntaxKind.QuestionToken),
factory.createAwaitExpression(queryCall),
queryCall,
factory.createToken(SyntaxKind.ColonToken),
factory.createIdentifier(getModelNameProp(lowerCaseDataTypeName)),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import { buildModelFieldObject } from './model-fields';
import { isModelDataType, shouldWrapInArrayField } from './render-checkers';
import { extractModelAndKeys, getMatchEveryModelFieldCallExpression } from './model-values';
import { COMPOSITE_PRIMARY_KEY_PROP_NAME, STORAGE_FILE_KEY } from '../../utils/constants';
import { DataApiKind } from '../../react-render-config';
import { getFetchRelatedRecords } from '../../utils/graphql';

export const buildMutationBindings = (form: StudioForm, primaryKeys: string[] = []) => {
const {
Expand Down Expand Up @@ -284,6 +286,7 @@ function getCallbackVarName(fieldType: string): string {
export const buildOnChangeStatement = (
component: StudioComponent | StudioComponentChild,
fieldConfigs: Record<string, FieldConfigMetadata>,
dataApi?: DataApiKind,
) => {
const { name: fieldName, componentType: fieldType } = component;
const fieldConfig = fieldConfigs[fieldName];
Expand All @@ -295,6 +298,16 @@ export const buildOnChangeStatement = (
...buildTargetVariable(studioFormComponentType || fieldType, renderedFieldName, dataType, isArray),
];

if (dataApi === 'GraphQL' && fieldConfig.relationship) {
handleChangeStatements.push(
factory.createExpressionStatement(
factory.createCallExpression(factory.createIdentifier(getFetchRelatedRecords(component.name)), undefined, [
factory.createIdentifier('value'),
]),
),
);
}

if (!shouldWrapInArrayField(fieldConfig)) {
handleChangeStatements.push(buildOverrideOnChangeStatement(fieldName, fieldConfigs));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ import {
getSetNameIdentifier,
buildUseStateExpression,
getControlledComponentDefaultValue,
buildInitConstVariableExpression,
} from '../../helpers';
import { getElementAccessExpression } from './invalid-variable-helpers';
import { shouldWrapInArrayField } from './render-checkers';
import { DataApiKind } from '../../react-render-config';

// used just to sanitize nested array field names
// when rendering currentValue state and ref
Expand All @@ -69,7 +71,8 @@ export const getCurrentValueName = (fieldName: string) => {
export const getCurrentDisplayValueName = (fieldName: string) =>
`current${capitalizeFirstLetter(fieldName)}DisplayValue`;

export const getRecordsName = (modelName: string) => `${lowerCaseFirst(modelName)}Records`;
export const getRecordsName = (modelName: string, capitalized = false) =>
`${(capitalized ? capitalizeFirstLetter : lowerCaseFirst)(modelName)}Records`;

export const getRecordName = (modelName: string) => `${lowerCaseFirst(modelName)}Record`;

Expand Down Expand Up @@ -228,11 +231,15 @@ export const getInitialValues = (
/**
* iterates field configs to create useState hooks for each field
* @param fieldConfigs
* @param dataApi
* @returns
*/
export const getUseStateHooks = (fieldConfigs: Record<string, FieldConfigMetadata>): Statement[] => {
export const getUseStateHooks = (
fieldConfigs: Record<string, FieldConfigMetadata>,
dataApi?: DataApiKind,
): Statement[] => {
const stateNames = new Set();
return Object.entries(fieldConfigs).reduce<Statement[]>((acc, [name, { sanitizedFieldName }]) => {
return Object.entries(fieldConfigs).reduce<Statement[]>((acc, [name, { sanitizedFieldName, relationship }]) => {
const fieldName = name.split('.')[0];
const renderedFieldName = sanitizedFieldName || fieldName;

Expand All @@ -256,10 +263,31 @@ export const getUseStateHooks = (fieldConfigs: Record<string, FieldConfigMetadat
acc.push(buildUseStateExpression(renderedFieldName, renderCorrectUseStateValue()));
stateNames.add(renderedFieldName);
}

if (dataApi === 'GraphQL' && relationship) {
acc.push(buildUseStateExpression(`${renderedFieldName}Loading`, factory.createFalse()));
acc.push(buildUseStateExpression(`${renderedFieldName}Records`, factory.createArrayLiteralExpression([], false)));
}
return acc;
}, []);
};

export const getAutocompleteOptions = (
fieldConfigs: Record<string, FieldConfigMetadata>,
hasAutoCompleteField: boolean,
dataApi?: DataApiKind,
): Statement[] => {
if (
dataApi === 'GraphQL' &&
hasAutoCompleteField &&
Object.values(fieldConfigs).some(({ relationship }) => !!relationship)
) {
return [buildInitConstVariableExpression('autocompleteLength', factory.createNumericLiteral('10'))];
}

return [];
};

/**
* function used by the Clear/ Reset button
* it's a reset type but we also need to clear the state of the input fields as well
Expand Down
Loading

0 comments on commit 6c4742b

Please sign in to comment.