Skip to content

Commit

Permalink
feat: @mapsTo directive to enable renaming models while retaining d…
Browse files Browse the repository at this point in the history
…ata (#9340)

* chore: dumping progress

* feat: retain original table on type rename

* feat: map table name in dependent functions

* chore: little bit of cleanup

* fix: push failures for dependent functions

* feat: support table renames in mock

* test: refactor orig directive and add tests

* fix: test failures

* test: remove unneeded test file

* chore: update package version

* test: fix other tests

* chore: cleanup before PR

* chore: rename directive to mapsTo

* fix: searchable transformer with renamed model

* fix: auth and searchable table translation

* fix: more searchable issues and mock test

* chore: address PR comments

* test: add e2e tests and some refactoring

* fix: hopefully closer to having rename working with relations

* fix: rename works for existing data in hasOne, hasMany, and belongsTo relations

* chore: fix a few more merge issues

* fix: renamed relations except for manyToMany working

* feat: mapsTo with searchable

* chore: minor renaming

* fix: refactor to support multiple renamings per type

* fix: issue with transformer dependency chain

* test: bunch of tests plus start refactor

* fix: refactor complete with tests passing

* test: add test w/ multiple field renames on single model

* chore: fix dep

* test: add and fix tests

* fix: add de-dupe logic to field rename map

* chore: add comments and fix LGTM warnings

* chore: fix dep

* fix: searchable bug

* chore: fixup versions

* fix: address most PR comments

* test: add test for conflicting model name

* fix: add sync resolvers to mapped resolver list

* test: add sync resolver test

* fix: switching to lambda resolver for input mapping

* fix: update vtl for mapped input

* fix: revert searchable input changes

* chore: fix cdk iam package name

* fix: move mapsto slotting to after transformer

* fix: transform input args in searchable resolvers

* chore: use common util for setting transformedArgs

* test: update vtl snapshots for arg mapping

* test: update snapshots

* fix: destructive update prompt for mapsTo

* test: add mapsTo e2e tests

* test: remove log lines

* test: add/update tests for lambda vtl invoker

* fix: map @manytomany fields on server side

* fix: use transformed args filter in type.field resolver

* chore: move input mapping function to preUpdate slot

* fix: add sort and aggregates to lambda pre-check

* chore: address PR comments and LGTM errors
  • Loading branch information
edwardfoyle committed Jan 27, 2022
1 parent 7b56ea4 commit aedf45d
Show file tree
Hide file tree
Showing 83 changed files with 5,291 additions and 1,505 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,22 @@ const constructCFModelTableArnComponent_mock = constructCFModelTableArnComponent
const appsyncResourceName = 'mock_api';
const resourceName = 'storage';

constructCFModelTableNameComponent_mock.mockImplementation(() => {
constructCFModelTableNameComponent_mock.mockImplementation(async () => {
return {
'Fn::ImportValue': {
'Fn::Sub': `\${api${appsyncResourceName}GraphQLAPIIdOutput}:GetAtt:${resourceName.replace(`:${appsyncTableSuffix}`, 'Table')}:Name`,
},
};
});

constructCFModelTableArnComponent_mock.mockImplementation(() => {
constructCFModelTableArnComponent_mock.mockImplementation(async () => {
return [
'arn:aws:dynamodb:',
{ Ref: 'aws_region' },
':',
{ Ref: 'aws_accountId' },
':table/',
constructCFModelTableNameComponent(appsyncResourceName, resourceName, appsyncTableSuffix),
await constructCFModelTableNameComponent(appsyncResourceName, resourceName, appsyncTableSuffix),
];
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ const environmentMap = {
};
const envVarStringList = '';

getResourcesforCFN_mock.mockReturnValue(Promise.resolve({ permissionPolicies, cfnResources }));
generateEnvVariablesforCFN_mock.mockReturnValue(Promise.resolve({ dependsOn, environmentMap, envVarStringList }));
getResourcesforCFN_mock.mockResolvedValue({ permissionPolicies, cfnResources });
generateEnvVariablesforCFN_mock.mockResolvedValue({ dependsOn, environmentMap, envVarStringList });

test('update dependent functions', async () => {
jest.clearAllMocks();
Expand All @@ -104,7 +104,7 @@ test('update dependent functions', async () => {
},
},
});
await updateDependentFunctionsCfn((contextStub as unknown) as $TSContext, allResources, backendDir, modelsDeleted, apiResourceName);
await updateDependentFunctionsCfn(contextStub as unknown as $TSContext, allResources, backendDir, modelsDeleted, apiResourceName);
expect(updateCFNFileForResourcePermissions_mock.mock.calls[0][1]).toMatchSnapshot();
});

Expand Down Expand Up @@ -142,6 +142,6 @@ test('update dependent functions', async () => {
},
],
});
await updateDependentFunctionsCfn((contextStub as unknown) as $TSContext, allResources, backendDir, modelsDeleted, apiResourceName);
await updateDependentFunctionsCfn(contextStub as unknown as $TSContext, allResources, backendDir, modelsDeleted, apiResourceName);
expect(updateCFNFileForResourcePermissions_mock.mock.calls[0][1]).toMatchSnapshot();
});
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export async function getResourcesForCfn(context, resourceName, resourcePolicy,
if (resourceName.endsWith(appsyncTableSuffix)) {
resourcePolicy.providerPlugin = 'awscloudformation';
resourcePolicy.service = 'DynamoDB';
const dynamoDBTableARNComponents = constructCFModelTableArnComponent(appsyncResourceName, resourceName, appsyncTableSuffix);
const dynamoDBTableARNComponents = await constructCFModelTableArnComponent(appsyncResourceName, resourceName, appsyncTableSuffix);

// have to override the policy resource as Fn::ImportValue is needed to extract DynamoDB table arn
resourcePolicy.customPolicyResource = [
Expand All @@ -241,19 +241,29 @@ export async function getResourcesForCfn(context, resourceName, resourcePolicy,
);

// replace resource attributes for @model-backed dynamoDB tables
const cfnResources = resourceAttributes.map(attributes =>
attributes.resourceName && attributes.resourceName.endsWith(appsyncTableSuffix)
? {
resourceName: appsyncResourceName,
category: 'api',
attributes: ['GraphQLAPIIdOutput'],
needsAdditionalDynamoDBResourceProps: true,
// data to pass so we construct additional resourceProps for lambda envvar for @model back dynamoDB tables
_modelName: attributes.resourceName.replace(`:${appsyncTableSuffix}`, 'Table'),
_cfJoinComponentTableName: constructCFModelTableNameComponent(appsyncResourceName, attributes.resourceName, appsyncTableSuffix),
_cfJoinComponentTableArn: constructCFModelTableArnComponent(appsyncResourceName, attributes.resourceName, appsyncTableSuffix),
}
: attributes,
const cfnResources = await Promise.all<$TSAny>(
resourceAttributes.map(async attributes =>
attributes.resourceName?.endsWith(appsyncTableSuffix)
? {
resourceName: appsyncResourceName,
category: 'api',
attributes: ['GraphQLAPIIdOutput'],
needsAdditionalDynamoDBResourceProps: true,
// data to pass so we construct additional resourceProps for lambda envvar for @model back dynamoDB tables
_modelName: attributes.resourceName.replace(`:${appsyncTableSuffix}`, 'Table'),
_cfJoinComponentTableName: await constructCFModelTableNameComponent(
appsyncResourceName,
attributes.resourceName,
appsyncTableSuffix,
),
_cfJoinComponentTableArn: await constructCFModelTableArnComponent(
appsyncResourceName,
attributes.resourceName,
appsyncTableSuffix,
),
}
: attributes,
),
);
return { permissionPolicies, cfnResources };
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { appsyncTableSuffix } from './constants';
import { getAppSyncResourceName } from './appSyncHelper';
import * as path from 'path';
import { pathManager, readCFNTemplate, writeCFNTemplate } from 'amplify-cli-core';
import { $TSAny, pathManager, readCFNTemplate, writeCFNTemplate } from 'amplify-cli-core';
import { categoryName } from '../../../constants';
import { getTableNameForModel, readProjectConfiguration } from 'graphql-transformer-core';

const functionCloudFormationFilePath = (functionName: string) =>
path.join(pathManager.getBackendDirPath(), categoryName, functionName, `${functionName}-cloudformation-template.json`);
Expand Down Expand Up @@ -187,22 +188,23 @@ export function getIAMPolicies(resourceName, crudOptions) {
}

/** CF template component of join function { "Fn::Join": ["": THIS_PART ] } */
export function constructCFModelTableArnComponent(appsyncResourceName, resourceName, appsyncTableSuffix) {
export async function constructCFModelTableArnComponent(appsyncResourceName, resourceName, appsyncTableSuffix) {
return [
'arn:aws:dynamodb:',
{ Ref: 'AWS::Region' },
':',
{ Ref: 'AWS::AccountId' },
':table/',
constructCFModelTableNameComponent(appsyncResourceName, resourceName, appsyncTableSuffix),
];
await constructCFModelTableNameComponent(appsyncResourceName, resourceName, appsyncTableSuffix),
] as $TSAny[];
}

/** CF template component of join function { "Fn::Join": ["-": THIS_PART ] } */
export function constructCFModelTableNameComponent(appsyncResourceName, resourceName, appsyncTableSuffix) {
export async function constructCFModelTableNameComponent(appsyncResourceName, resourceName, appsyncTableSuffix) {
const tableName = await mapModelNameToTableName(resourceName.replace(`:${appsyncTableSuffix}`, ''));
return {
'Fn::ImportValue': {
'Fn::Sub': `\${api${appsyncResourceName}GraphQLAPIIdOutput}:GetAtt:${resourceName.replace(`:${appsyncTableSuffix}`, 'Table')}:Name`,
'Fn::Sub': `\${api${appsyncResourceName}GraphQLAPIIdOutput}:GetAtt:${tableName}Table:Name`,
},
};
}
Expand Down Expand Up @@ -261,3 +263,10 @@ function apiResourceAddCheck(currentResources, newResources, apiResourceName, re
isEnvParams ? currentResources.push(`API_${apiResourceName.toUpperCase()}_`) : currentResources.push(`api${apiResourceName}`);
}
}

async function mapModelNameToTableName(modelName: string): Promise<string> {
const appSyncResourceName = getAppSyncResourceName();
const resourceDirPath = path.join(pathManager.getBackendDirPath(), 'api', appSyncResourceName);
const project = await readProjectConfiguration(resourceDirPath);
return getTableNameForModel(project.schema, modelName);
}
3 changes: 2 additions & 1 deletion packages/amplify-category-function/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
},
"references": [
{"path": "../amplify-function-plugin-interface"},
{"path": "../amplify-cli-core"}
{"path": "../amplify-cli-core"},
{"path": "../graphql-transformer-core"}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -613,11 +613,10 @@ Static group authorization should perform as expected.`,
): void => {
const acmFields = acm.getResources();
const modelFields = def.fields ?? [];
const name = acm.getName();
// only add readonly fields if they exist
const allowedAggFields = modelFields.map(f => f.name.value).filter(f => !acmFields.includes(f));
let leastAllowedFields = acmFields;
const resolver = ctx.resolvers.getResolver('Search', toUpper(name)) as TransformerResolverProvider;
const resolver = ctx.resolvers.getResolver(typeName, fieldName) as TransformerResolverProvider;
// to protect search and aggregation queries we need to collect all the roles which can query
// and the allowed fields to run field auth on aggregation queries
const readRoleDefinitions = acm.getRolesPerOperation('read').map(role => {
Expand Down

0 comments on commit aedf45d

Please sign in to comment.