Skip to content

Commit

Permalink
fix(graphql-model-transformer): override resource logical id to fix v…
Browse files Browse the repository at this point in the history
…1 to v2 transformer migration (#8597)
  • Loading branch information
lazpavel committed Oct 31, 2021
1 parent 4d50df0 commit e3a2afb
Show file tree
Hide file tree
Showing 24 changed files with 1,226 additions and 813 deletions.
@@ -0,0 +1,19 @@
type Post @model {
id: ID!
title: String!
}

type Customer @model @key(fields: ["email"]) @key(name: "byUsername", fields: ["username"], queryField: "byUsername") {
email: String!
username: String
}

type Test @model(timestamps: { createdAt: "createdOn", updatedAt: "updatedOn" }) {
id: ID!
title: String!
}

type Rename @model(queries: { get: "rename" }, mutations: { create: "makeRename" }, subscriptions: null) {
id: ID!
title: String!
}
@@ -0,0 +1,19 @@
type Post @model @auth(rules: [{ allow: public }]) {
id: ID!
title: String!
}

type Customer @model @auth(rules: [{ allow: public }]) {
email: String! @primaryKey
username: String @index(name: "byUsername", queryField: "byUsername")
}

type Test @model(timestamps: { createdAt: "createdOn", updatedAt: "updatedOn" }) @auth(rules: [{ allow: public }]) {
id: ID!
title: String!
}

type Rename @model(queries: { get: "rename" }, mutations: { create: "makeRename" }, subscriptions: null) @auth(rules: [{ allow: public }]) {
id: ID!
title: String!
}
30 changes: 30 additions & 0 deletions packages/amplify-e2e-tests/src/GraphQLClient.ts
@@ -0,0 +1,30 @@
import axios from 'axios';

export interface GraphQLLocation {
line: number;
column: number;
}
export interface GraphQLError {
message: string;
locations: GraphQLLocation[];
path: string[];
}
export interface GraphQLResponse {
data: any;
errors: GraphQLError[];
}
export class GraphQLClient {
constructor(private url: string, private headers: any) {}

async query(query: string, variables: any = {}): Promise<GraphQLResponse> {
const axRes = await axios.post<GraphQLResponse>(
this.url,
{
query,
variables,
},
{ headers: this.headers },
);
return axRes.data;
}
}
@@ -0,0 +1,272 @@
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 AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync';
import gql from 'graphql-tag';
(global as any).fetch = require('node-fetch');

describe('transformer model migration test', () => {
let projRoot: string;
let projectName: string;

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 model key queries timestamps should succeed', async () => {
const modelSchemaV1 = 'transformer_migration/basic-model-v1.graphql';
const modelSchemaV2 = 'transformer_migration/basic-model-v2.graphql';

await addApiWithoutSchema(projRoot, { apiName: projectName });
await updateApiSchema(projRoot, projectName, modelSchemaV1);
await amplifyPush(projRoot);

let appSyncClient = getAppSyncClientFromProj(projRoot);

let createPostMutation = /* GraphQL */ `
mutation CreatePost {
createPost(input: { title: "Created in V1" }) {
id
}
}
`;

let createPostResult = await appSyncClient.mutate({
mutation: gql(createPostMutation),
fetchPolicy: 'no-cache',
});

expect(createPostResult.errors).toBeUndefined();
expect(createPostResult.data).toBeDefined();

let createCustomerMutation = /* GraphQL */ `
mutation CreateCustomer {
createCustomer(input: { email: "v1@test.com" }) {
email
}
}
`;

let createCustomerResult = await appSyncClient.mutate({
mutation: gql(createCustomerMutation),
fetchPolicy: 'no-cache',
});

expect(createCustomerResult.errors).toBeUndefined();
expect(createCustomerResult.data).toBeDefined();

let createTestMutation = /* GraphQL */ `
mutation CreateTest {
createTest(input: { title: "Created in V1" }) {
id
}
}
`;

let createTestResult = await appSyncClient.mutate({
mutation: gql(createTestMutation),
fetchPolicy: 'no-cache',
});

expect(createTestResult.errors).toBeUndefined();
expect(createTestResult.data).toBeDefined();

let createRenameMutation = /* GraphQL */ `
mutation CreateRename {
makeRename(input: { title: "Created in V1" }) {
id
}
}
`;

let createRenameResult = await appSyncClient.mutate({
mutation: gql(createRenameMutation),
fetchPolicy: 'no-cache',
});

expect(createRenameResult.errors).toBeUndefined();
expect(createRenameResult.data).toBeDefined();

await addFeatureFlag(projRoot, 'graphqltransformer', 'transformerVersion', 2);
await addFeatureFlag(projRoot, 'graphqltransformer', 'useExperimentalPipelinedTransformer', true);

await updateApiSchema(projRoot, projectName, modelSchemaV2);
await amplifyPushUpdate(projRoot);

appSyncClient = getAppSyncClientFromProj(projRoot);

createPostMutation = /* GraphQL */ `
mutation CreatePost {
createPost(input: { title: "Created in V2" }) {
id
}
}
`;

createPostResult = await appSyncClient.mutate({
mutation: gql(createPostMutation),
fetchPolicy: 'no-cache',
});

expect(createPostResult.errors).toBeUndefined();
expect(createPostResult.data).toBeDefined();

createCustomerMutation = /* GraphQL */ `
mutation CreateCustomer {
createCustomer(input: { email: "v2@test.com" }) {
email
}
}
`;

createCustomerResult = await appSyncClient.mutate({
mutation: gql(createCustomerMutation),
fetchPolicy: 'no-cache',
});

expect(createCustomerResult.errors).toBeUndefined();
expect(createCustomerResult.data).toBeDefined();

createTestMutation = /* GraphQL */ `
mutation CreateTest {
createTest(input: { title: "Created in V2" }) {
id
}
}
`;

createTestResult = await appSyncClient.mutate({
mutation: gql(createTestMutation),
fetchPolicy: 'no-cache',
});

expect(createTestResult.errors).toBeUndefined();
expect(createTestResult.data).toBeDefined();

createRenameMutation = /* GraphQL */ `
mutation CreateRename {
makeRename(input: { title: "Created in V2" }) {
id
}
}
`;

createRenameResult = await appSyncClient.mutate({
mutation: gql(createRenameMutation),
fetchPolicy: 'no-cache',
});

expect(createRenameResult.errors).toBeUndefined();
expect(createRenameResult.data).toBeDefined();

const postsQuery = /* GraphQL */ `
query ListPosts {
listPosts {
items {
id
title
}
}
}
`;

let queryResult = await appSyncClient.query({
query: gql(postsQuery),
fetchPolicy: 'no-cache',
});

expect(queryResult.errors).toBeUndefined();
expect(queryResult.data).toBeDefined();
expect((queryResult.data as any).listPosts.items.length).toEqual(2);

const customersQuery = /* GraphQL */ `
query ListCustomers {
listCustomers {
items {
email
}
}
}
`;

queryResult = await appSyncClient.query({
query: gql(customersQuery),
fetchPolicy: 'no-cache',
});

expect(queryResult.errors).toBeUndefined();
expect(queryResult.data).toBeDefined();
expect((queryResult.data as any).listCustomers.items.length).toEqual(2);

const testsQuery = /* GraphQL */ `
query ListTests {
listTests {
items {
id
title
}
}
}
`;

queryResult = await appSyncClient.query({
query: gql(testsQuery),
fetchPolicy: 'no-cache',
});

expect(queryResult.errors).toBeUndefined();
expect(queryResult.data).toBeDefined();
expect((queryResult.data as any).listTests.items.length).toEqual(2);

const renamesQuery = /* GraphQL */ `
query GetRename {
rename (id: "${(createRenameResult.data as any).makeRename.id}") {
id
title
}
}
`;

queryResult = await appSyncClient.query({
query: gql(renamesQuery),
fetchPolicy: 'no-cache',
});

expect(queryResult.errors).toBeUndefined();
expect(queryResult.data).toBeDefined();
});

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,
},
});
};
});
Expand Up @@ -66,7 +66,15 @@ import {
} from 'graphql';
import { SubscriptionLevel, ModelDirectiveConfiguration } from '@aws-amplify/graphql-model-transformer';
import { AccessControlMatrix } from './accesscontrol';
import { getBaseType, makeDirective, makeField, makeNamedType, ResourceConstants, ModelResourceIDs } from 'graphql-transformer-common';
import {
getBaseType,
makeDirective,
makeField,
makeNamedType,
ResourceConstants,
ModelResourceIDs,
ResolverResourceIDs,
} from 'graphql-transformer-common';
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';
import {
Expand Down Expand Up @@ -649,6 +657,7 @@ Static group authorization should perform as expected.`,
new TransformerResolver(
typeName,
fieldName,
ResolverResourceIDs.ResolverResourceID(typeName, fieldName),
MappingTemplate.s3MappingTemplateFromString(fieldAuthExpression, `${typeName}.${fieldName}.req.vtl`),
MappingTemplate.s3MappingTemplateFromString(fieldResponse, `${typeName}.${fieldName}.res.vtl`),
['init'],
Expand Down
13 changes: 10 additions & 3 deletions packages/amplify-graphql-auth-transformer/src/utils/schema.ts
Expand Up @@ -23,6 +23,7 @@ import {
} from 'graphql-transformer-common';
import { RELATIONAL_DIRECTIVES } from './constants';
import { RelationalPrimaryMapConfig, RoleDefinition, SearchableConfig } from './definitions';
import md5 from 'md5';

export const collectFieldNames = (object: ObjectTypeDefinitionNode): Array<string> => {
return object.fields!.map((field: FieldDefinitionNode) => field.name.value);
Expand All @@ -47,9 +48,9 @@ export const getModelConfig = (directive: DirectiveNode, typeName: string, isDat
},
subscriptions: {
level: SubscriptionLevel.on,
onCreate: [toCamelCase(['onCreate', typeName])],
onDelete: [toCamelCase(['onDelete', typeName])],
onUpdate: [toCamelCase(['onUpdate', typeName])],
onCreate: [ensureValidSubscriptionName(toCamelCase(['onCreate', typeName]))],
onDelete: [ensureValidSubscriptionName(toCamelCase(['onDelete', typeName]))],
onUpdate: [ensureValidSubscriptionName(toCamelCase(['onUpdate', typeName]))],
},
timestamps: {
createdAt: 'createdAt',
Expand Down Expand Up @@ -335,3 +336,9 @@ export const getSubscriptionFieldNames = (

return fields;
};

const ensureValidSubscriptionName = (name: string): string => {
if (name.length <= 50) return name;

return name.slice(0, 45) + md5(name).slice(0, 5);
};

0 comments on commit e3a2afb

Please sign in to comment.