diff --git a/packages/amplify-e2e-tests/schemas/transformer_migration/searchable-v1.graphql b/packages/amplify-e2e-tests/schemas/transformer_migration/searchable-v1.graphql new file mode 100644 index 00000000000..1c37355b9ae --- /dev/null +++ b/packages/amplify-e2e-tests/schemas/transformer_migration/searchable-v1.graphql @@ -0,0 +1,6 @@ +type Todo @model @searchable { + id: ID! + name: String! + description: String + count: Int +} \ No newline at end of file diff --git a/packages/amplify-e2e-tests/schemas/transformer_migration/searchable-v2.graphql b/packages/amplify-e2e-tests/schemas/transformer_migration/searchable-v2.graphql new file mode 100644 index 00000000000..054837ae513 --- /dev/null +++ b/packages/amplify-e2e-tests/schemas/transformer_migration/searchable-v2.graphql @@ -0,0 +1,6 @@ +type Todo @model @searchable @auth(rules: [{ allow: public }]) { + id: ID! + name: String! + description: String + count: Int +} \ No newline at end of file diff --git a/packages/amplify-e2e-tests/src/__tests__/transformer-migrations/searchable-migration.test.ts b/packages/amplify-e2e-tests/src/__tests__/transformer-migrations/searchable-migration.test.ts new file mode 100644 index 00000000000..06a378d53c2 --- /dev/null +++ b/packages/amplify-e2e-tests/src/__tests__/transformer-migrations/searchable-migration.test.ts @@ -0,0 +1,115 @@ +import { + initJSProjectWithProfile, + deleteProject, + amplifyPush, + amplifyPushUpdate, + addFeatureFlag, + createRandomName, + addAuthWithDefault, +} from 'amplify-e2e-core'; +import { addApiWithoutSchema, updateApiSchema, getProjectMeta } from 'amplify-e2e-core'; +import { createNewProjectDir, deleteProjectDir } from 'amplify-e2e-core'; +import gql from 'graphql-tag'; +import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync'; +(global as any).fetch = require('node-fetch'); + +describe('transformer model searchable migration test', () => { + let projRoot: string; + let projectName: string; + let appSyncClient = undefined; + + beforeEach(async () => { + projectName = createRandomName(); + projRoot = await createNewProjectDir(createRandomName()); + await initJSProjectWithProfile(projRoot, { + name: projectName, + }); + await addAuthWithDefault(projRoot, {}); + }); + + afterEach(async () => { + await deleteProject(projRoot); + deleteProjectDir(projRoot); + }); + + it('migration of searchable directive - search should return expected results', async () => { + const v1Schema = 'transformer_migration/searchable-v1.graphql'; + const v2Schema = 'transformer_migration/searchable-v2.graphql'; + + await addApiWithoutSchema(projRoot, { apiName: projectName }); + await updateApiSchema(projRoot, projectName, v1Schema); + await amplifyPush(projRoot); + + appSyncClient = getAppSyncClientFromProj(projRoot); + await runAndValidateQuery('test1', 'test1', 10); + + await addFeatureFlag(projRoot, 'graphqltransformer', 'transformerVersion', 2); + await addFeatureFlag(projRoot, 'graphqltransformer', 'useExperimentalPipelinedTransformer', true); + + await updateApiSchema(projRoot, projectName, v2Schema); + await amplifyPushUpdate(projRoot); + + appSyncClient = getAppSyncClientFromProj(projRoot); + await runAndValidateQuery('test2', 'test2', 10); + }); + + const getAppSyncClientFromProj = (projRoot: string) => { + const meta = getProjectMeta(projRoot); + const region = meta['providers']['awscloudformation']['Region'] as string; + const { output } = meta.api[projectName]; + const url = output.GraphQLAPIEndpointOutput as string; + const apiKey = output.GraphQLAPIKeyOutput as string; + + return new AWSAppSyncClient({ + url, + region, + disableOffline: true, + auth: { + type: AUTH_TYPE.API_KEY, + apiKey, + }, + }); + }; + + const fragments = [`fragment FullTodo on Todo { id name description count }`]; + + const runMutation = async (query: string) => { + try { + const q = [query, ...fragments].join('\n'); + const response = await appSyncClient.mutate({ + mutation: gql(q), + fetchPolicy: 'no-cache', + }); + return response; + } catch (e) { + console.error(e); + return null; + } + }; + + const createEntry = async (name: string, description: string, count: number) => { + return await runMutation(getCreateTodosMutation(name, description, count)); + }; + + function getCreateTodosMutation( + name: string, + description: string, + count: number, + ): string { + return `mutation { + createTodo(input: { + name: "${name}" + description: "${description}" + count: ${count} + }) { ...FullTodo } + }`; + } + + const runAndValidateQuery = async (name: string, description: string, count: number) => { + const response = await createEntry(name, description, count); + expect(response).toBeDefined(); + expect(response.errors).toBeUndefined(); + expect(response.data).toBeDefined(); + expect(response.data.createTodo).toBeDefined(); + } +}); diff --git a/packages/amplify-graphql-auth-transformer/src/accesscontrol/acm.ts b/packages/amplify-graphql-auth-transformer/src/accesscontrol/acm.ts index a70d1ca1fd5..8b8269f5c92 100644 --- a/packages/amplify-graphql-auth-transformer/src/accesscontrol/acm.ts +++ b/packages/amplify-graphql-auth-transformer/src/accesscontrol/acm.ts @@ -69,6 +69,10 @@ export class AccessControlMatrix { return this.roles.includes(role); } + public getName(): string { + return this.name; + } + public getRoles(): Array { return this.roles; } diff --git a/packages/amplify-graphql-auth-transformer/src/graphql-auth-transformer.ts b/packages/amplify-graphql-auth-transformer/src/graphql-auth-transformer.ts index cf78f271a90..f252f8587d0 100644 --- a/packages/amplify-graphql-auth-transformer/src/graphql-auth-transformer.ts +++ b/packages/amplify-graphql-auth-transformer/src/graphql-auth-transformer.ts @@ -581,10 +581,11 @@ 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(typeName, fieldName) as TransformerResolverProvider; + const resolver = ctx.resolvers.getResolver('Search', toUpper(name)) 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 => { diff --git a/packages/amplify-graphql-searchable-transformer/src/cdk/create-cfnParameters.ts b/packages/amplify-graphql-searchable-transformer/src/cdk/create-cfnParameters.ts index 66dcb2bbc1f..a2294a2909a 100644 --- a/packages/amplify-graphql-searchable-transformer/src/cdk/create-cfnParameters.ts +++ b/packages/amplify-graphql-searchable-transformer/src/cdk/create-cfnParameters.ts @@ -72,7 +72,7 @@ export function createParametersStack(stack: Stack): Map { OpenSearchAccessIAMRoleName, new CfnParameter(stack, OpenSearchStreamingIAMRoleName, { description: 'The name of the streaming lambda function IAM role.', - default: 'SearchableLambdaIAMRole', + default: 'SearchLambdaIAMRole', }), ], diff --git a/packages/amplify-graphql-searchable-transformer/src/graphql-searchable-transformer.ts b/packages/amplify-graphql-searchable-transformer/src/graphql-searchable-transformer.ts index f66183a39d5..f4b4e278af6 100644 --- a/packages/amplify-graphql-searchable-transformer/src/graphql-searchable-transformer.ts +++ b/packages/amplify-graphql-searchable-transformer/src/graphql-searchable-transformer.ts @@ -135,7 +135,7 @@ export class SearchableModelTransformer extends TransformerPluginBase { const resolver = context.resolvers.generateQueryResolver( typeName, def.fieldName, - ResolverResourceIDs.ResolverResourceID(typeName, def.fieldName), + ResolverResourceIDs.ElasticsearchSearchResolverResourceID(type), datasource as DataSourceProvider, MappingTemplate.s3MappingTemplateFromString( requestTemplate(attributeName, getNonKeywordFields(def.node), false, type, keyFields), @@ -151,7 +151,7 @@ export class SearchableModelTransformer extends TransformerPluginBase { ), ); resolver.mapToStack(stack); - context.resolvers.addResolver(typeName, def.fieldName, resolver); + context.resolvers.addResolver('Search', toUpper(type), resolver); } createStackOutputs(stack, domain.domainEndpoint, context.api.apiId, domain.domainArn);