Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: headless api migration #8992

Merged
merged 7 commits into from
Nov 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/amplify-category-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
AmplifyCategories,
AmplifySupportedService,
buildOverrideDir,
exitOnNextTick,
pathManager,
stateManager,
} from 'amplify-cli-core';
Expand All @@ -18,6 +19,8 @@ import { ApigwStackTransform } from './provider-utils/awscloudformation/cdk-stac
import { getCfnApiArtifactHandler } from './provider-utils/awscloudformation/cfn-api-artifact-handler';
import { askAuthQuestions } from './provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough';
import { authConfigToAppSyncAuthType } from './provider-utils/awscloudformation/utils/auth-config-to-app-sync-auth-type-bi-di-mapper';
import { checkAppsyncApiResourceMigration } from './provider-utils/awscloudformation/utils/check-appsync-api-migration';
import { getAppSyncApiResourceName } from './provider-utils/awscloudformation/utils/getAppSyncApiName';
export { NETWORK_STACK_LOGICAL_ID } from './category-constants';
export { addAdminQueriesApi, updateAdminQueriesApi } from './provider-utils/awscloudformation/';
export { DEPLOYMENT_MECHANISM } from './provider-utils/awscloudformation/base-api-stack';
Expand Down Expand Up @@ -235,6 +238,11 @@ export const executeAmplifyHeadlessCommand = async (context: $TSContext, headles
await getCfnApiArtifactHandler(context).createArtifacts(await validateAddApiRequest(headlessPayload));
break;
case 'update':
const resourceName = await getAppSyncApiResourceName(context);
if (!(await checkAppsyncApiResourceMigration(context, resourceName, true))) {
printer.error('Update operations only work on migrated projects. Run "amplify update api" and opt for migration.');
exitOnNextTick(0);
}
await getCfnApiArtifactHandler(context).updateArtifacts(await validateUpdateApiRequest(headlessPayload));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does validateUpdateApiRequest return a bool ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it return a payload , other wise will throw error , if the payload doesn't matches schema

break;
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { $TSContext, AmplifySupportedService } from 'amplify-cli-core';

export const getAppSyncApiResourceName = async (context: $TSContext): Promise<string> => {
const { allResources } = await context.amplify.getResourceStatus();
const apiResource = allResources.filter((resource: { service: string }) => resource.service === AmplifySupportedService.APPSYNC);
let apiResourceName;

if (apiResource.length > 0) {
sachscode marked this conversation as resolved.
Show resolved Hide resolved
const resource = apiResource[0];
apiResourceName = resource.resourceName;
} else {
throw new Error(`${AmplifySupportedService.APPSYNC} API does not exist. To add an api, use "amplify update api".`);
}
return apiResourceName;
};
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export async function pushResources(
// building all CFN stacks here to get the resource Changes
await generateDependentResourcesType(context);
const resourcesToBuild: IAmplifyResource[] = await getResources(context);
context.amplify.executeProviderUtils(context, 'awscloudformation', 'buildOverrides', {
await context.amplify.executeProviderUtils(context, 'awscloudformation', 'buildOverrides', {
resourcesToBuild,
forceCompile: true,
});
Expand Down Expand Up @@ -102,7 +102,9 @@ export async function pushResources(
}
if (!retryPush) {
if (isAuthError) {
printer.warn(`You defined authorization rules (@auth) but haven't enabled their authorization providers on your GraphQL API. Run "amplify update api" to configure your GraphQL API to include the appropriate authorization providers as an authorization mode.`);
printer.warn(
`You defined authorization rules (@auth) but haven't enabled their authorization providers on your GraphQL API. Run "amplify update api" to configure your GraphQL API to include the appropriate authorization providers as an authorization mode.`,
);
printer.error(err.message);
}
throw err;
Expand Down
20 changes: 14 additions & 6 deletions packages/amplify-e2e-core/src/categories/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,13 @@ export function updateApiSchema(cwd: string, projectName: string, schemaName: st

export function updateApiWithMultiAuth(cwd: string, settings: any) {
return new Promise<void>((resolve, reject) => {
spawn(getCLIPath(settings.testingWithLatestCodebase), ['update', 'api'], { cwd, stripColors: true })
.wait('Select from one of the below mentioned services:')
.sendCarriageReturn()
const testingWithLatestCodebase = settings?.testingWithLatestCodebase ?? false;
const chain = spawn(getCLIPath(testingWithLatestCodebase), ['update', 'api'], { cwd, stripColors: true });
chain.wait('Select from one of the below mentioned services:').sendCarriageReturn();
if (testingWithLatestCodebase === true) {
chain.wait('Do you want to migrate api resource').sendConfirmYes();
}
chain
.wait(/.*Select a setting to edit.*/)
.sendCarriageReturn()
.wait(/.*Choose the default authorization type for the API.*/)
Expand Down Expand Up @@ -378,9 +382,13 @@ export function updateAPIWithResolutionStrategyWithoutModels(cwd: string, settin

export function updateAPIWithResolutionStrategyWithModels(cwd: string, settings: any) {
return new Promise<void>((resolve, reject) => {
spawn(getCLIPath(settings.testingWithLatestCodebase), ['update', 'api'], { cwd, stripColors: true })
.wait('Select from one of the below mentioned services:')
.sendCarriageReturn()
const testingWithLatestCodebase = settings?.testingWithLatestCodebase ?? false;
const chain = spawn(getCLIPath(testingWithLatestCodebase), ['update', 'api'], { cwd, stripColors: true });
chain.wait('Select from one of the below mentioned services:').sendCarriageReturn();
if (testingWithLatestCodebase === true) {
chain.wait('Do you want to migrate api resource').sendYes();
}
chain
.wait(/.*Select a setting to edit.*/)
.sendKeyDown()
.sendCarriageReturn()
Expand Down
8 changes: 6 additions & 2 deletions packages/amplify-e2e-core/src/utils/headless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ import {
import execa, { ExecaChildProcess } from 'execa';
import { getCLIPath } from '..';

export const addHeadlessApi = async (cwd: string, request: AddApiRequest): Promise<ExecaChildProcess<String>> => {
return await executeHeadlessCommand(cwd, 'api', 'add', request);
export const addHeadlessApi = async (cwd: string, request: AddApiRequest, settings?: any): Promise<ExecaChildProcess<String>> => {
const allowDestructiveUpdates = settings?.allowDestructiveUpdates ?? false;
const testingWithLatestCodebase = settings?.testingWithLatestCodebase ?? false;
return executeHeadlessCommand(cwd, 'api', 'add', request, true, allowDestructiveUpdates, {
testingWithLatestCodebase: testingWithLatestCodebase,
});
};

export const updateHeadlessApi = async (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`api migration update test updates AppSync API in headless mode 1`] = `
Object {
"ElasticsearchWarning": true,
"ResolverConfig": Object {
"project": Object {
"ConflictDetection": "VERSION",
"ConflictHandler": "OPTIMISTIC_CONCURRENCY",
},
},
"Version": 5,
}
`;

exports[`api migration update test updates AppSync API in headless mode 2`] = `
Object {
"additionalAuthenticationProviders": Array [
Object {
"apiKeyConfig": Object {},
"authenticationType": "API_KEY",
},
],
"defaultAuthentication": Object {
"authenticationType": "AWS_IAM",
},
}
`;

exports[`api migration update test updates AppSync API in headless mode 3`] = `
"type Todo @model {
id: ID!
content: String
override: String
}
"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import {
addApiWithoutSchema,
addApiWithBlankSchemaAndConflictDetection,
amplifyPush,
amplifyPushUpdate,
createNewProjectDir,
deleteProject,
deleteProjectDir,
getAppSyncApi,
getProjectMeta,
getTransformConfig,
updateApiSchema,
updateApiWithMultiAuth,
updateAPIWithResolutionStrategyWithModels,
addHeadlessApi,
updateHeadlessApi,
getProjectSchema,
getSchemaPath,
getCLIInputs,
initJSProjectWithProfile,
} from 'amplify-e2e-core';
import { AddApiRequest, UpdateApiRequest } from 'amplify-headless-interface';
import * as fs from 'fs-extra';
import { TRANSFORM_BASE_VERSION, TRANSFORM_CURRENT_VERSION } from 'graphql-transformer-core';
import { join } from 'path';

describe('api migration update test', () => {
let projRoot: string;
beforeEach(async () => {
projRoot = await createNewProjectDir('graphql-api');
});

afterEach(async () => {
const metaFilePath = join(projRoot, 'amplify', '#current-cloud-backend', 'amplify-meta.json');
if (fs.existsSync(metaFilePath)) {
await deleteProject(projRoot);
}
deleteProjectDir(projRoot);
});

it('api update migration with multiauth', async () => {
// init and add api with installed CLI
await initJSProjectWithProfile(projRoot, { name: 'simplemodelmultiauth' });
await addApiWithoutSchema(projRoot);
await updateApiSchema(projRoot, 'simplemodelmultiauth', 'simple_model.graphql');
await amplifyPush(projRoot);
// update and push with codebase
await updateApiWithMultiAuth(projRoot, { testingWithLatestCodebase: true });
// cli-inputs should exist
expect(getCLIInputs(projRoot, 'api', 'simplemodelmultiauth')).toBeDefined();
await amplifyPushUpdate(projRoot, undefined, true, true);

const meta = getProjectMeta(projRoot);
const { output } = meta.api.simplemodelmultiauth;
const { GraphQLAPIIdOutput, GraphQLAPIEndpointOutput, GraphQLAPIKeyOutput } = output;
const { graphqlApi } = await getAppSyncApi(GraphQLAPIIdOutput, meta.providers.awscloudformation.Region);

expect(graphqlApi).toBeDefined();
expect(graphqlApi.authenticationType).toEqual('API_KEY');
expect(graphqlApi.additionalAuthenticationProviders).toHaveLength(3);
expect(graphqlApi.additionalAuthenticationProviders).toHaveLength(3);

const cognito = graphqlApi.additionalAuthenticationProviders.filter(a => a.authenticationType === 'AMAZON_COGNITO_USER_POOLS')[0];

expect(cognito).toBeDefined();
expect(cognito.userPoolConfig).toBeDefined();

const iam = graphqlApi.additionalAuthenticationProviders.filter(a => a.authenticationType === 'AWS_IAM')[0];

expect(iam).toBeDefined();

const oidc = graphqlApi.additionalAuthenticationProviders.filter(a => a.authenticationType === 'OPENID_CONNECT')[0];

expect(oidc).toBeDefined();
expect(oidc.openIDConnectConfig).toBeDefined();
expect(oidc.openIDConnectConfig.issuer).toEqual('https://facebook.com/');
expect(oidc.openIDConnectConfig.clientId).toEqual('clientId');
expect(oidc.openIDConnectConfig.iatTTL).toEqual(1000);
expect(oidc.openIDConnectConfig.authTTL).toEqual(2000);

expect(GraphQLAPIIdOutput).toBeDefined();
expect(GraphQLAPIEndpointOutput).toBeDefined();
expect(GraphQLAPIKeyOutput).toBeDefined();

expect(graphqlApi).toBeDefined();
expect(graphqlApi.apiId).toEqual(GraphQLAPIIdOutput);
});

it('init a sync enabled project and update conflict resolution strategy', async () => {
const name = `syncenabled`;
// init and add api with locally installed cli
await initJSProjectWithProfile(projRoot, { name });
await addApiWithBlankSchemaAndConflictDetection(projRoot);
await updateApiSchema(projRoot, name, 'simple_model.graphql');
await amplifyPush(projRoot);
let transformConfig = getTransformConfig(projRoot, name);
expect(transformConfig).toBeDefined();
expect(transformConfig.ResolverConfig).toBeDefined();
expect(transformConfig.ResolverConfig.project).toBeDefined();
expect(transformConfig.ResolverConfig.project.ConflictDetection).toEqual('VERSION');
expect(transformConfig.ResolverConfig.project.ConflictHandler).toEqual('AUTOMERGE');

//update and push with codebase
await updateAPIWithResolutionStrategyWithModels(projRoot, { testingWithLatestCodebase: true });
expect(getCLIInputs(projRoot, 'api', 'syncenabled')).toBeDefined();
transformConfig = getTransformConfig(projRoot, name);
expect(transformConfig).toBeDefined();
expect(transformConfig.Version).toBeDefined();
expect(transformConfig.Version).toEqual(TRANSFORM_CURRENT_VERSION);
expect(transformConfig.ResolverConfig).toBeDefined();
expect(transformConfig.ResolverConfig.project).toBeDefined();
expect(transformConfig.ResolverConfig.project.ConflictDetection).toEqual('VERSION');
expect(transformConfig.ResolverConfig.project.ConflictHandler).toEqual('OPTIMISTIC_CONCURRENCY');

await amplifyPushUpdate(projRoot, undefined, true, true);
const meta = getProjectMeta(projRoot);
const { output } = meta.api[name];
const { GraphQLAPIIdOutput, GraphQLAPIEndpointOutput, GraphQLAPIKeyOutput } = output;
const { graphqlApi } = await getAppSyncApi(GraphQLAPIIdOutput, meta.providers.awscloudformation.Region);

expect(GraphQLAPIIdOutput).toBeDefined();
expect(GraphQLAPIEndpointOutput).toBeDefined();
expect(GraphQLAPIKeyOutput).toBeDefined();

expect(graphqlApi).toBeDefined();
expect(graphqlApi.apiId).toEqual(GraphQLAPIIdOutput);
});

const addApiRequest: AddApiRequest = {
version: 1,
serviceConfiguration: {
serviceName: 'AppSync',
apiName: 'myApiName',
transformSchema: fs.readFileSync(getSchemaPath('simple_model.graphql'), 'utf8'),
defaultAuthType: {
mode: 'API_KEY',
},
},
};

const updateApiRequest: UpdateApiRequest = {
version: 1,
serviceModification: {
serviceName: 'AppSync',
transformSchema: fs.readFileSync(getSchemaPath('simple_model_override.graphql'), 'utf8'),
defaultAuthType: {
mode: 'AWS_IAM',
},
additionalAuthTypes: [
{
mode: 'API_KEY',
},
],
conflictResolution: {
defaultResolutionStrategy: {
type: 'OPTIMISTIC_CONCURRENCY',
},
},
},
};
it('updates AppSync API in headless mode', async () => {
const name = `simplemodelv${TRANSFORM_BASE_VERSION}`;
await initJSProjectWithProfile(projRoot, {});
await addHeadlessApi(projRoot, addApiRequest, {
allowDestructiveUpdates: false,
testingWithLatestCodebase: false,
});
await amplifyPush(projRoot);
await updateHeadlessApi(projRoot, updateApiRequest, true);
expect(getCLIInputs(projRoot, 'api', 'myApiName')).toBeDefined();
await amplifyPushUpdate(projRoot, undefined, undefined, true);

//verify
const meta = getProjectMeta(projRoot);
const { output } = meta.api.myApiName;
const { GraphQLAPIIdOutput, GraphQLAPIEndpointOutput, GraphQLAPIKeyOutput } = output;
const { graphqlApi } = await getAppSyncApi(GraphQLAPIIdOutput, meta.providers.awscloudformation.Region);

expect(GraphQLAPIIdOutput).toBeDefined();
expect(GraphQLAPIEndpointOutput).toBeDefined();
expect(GraphQLAPIKeyOutput).toBeDefined();

expect(graphqlApi).toBeDefined();
expect(graphqlApi.apiId).toEqual(GraphQLAPIIdOutput);

expect(getTransformConfig(projRoot, 'myApiName')).toMatchSnapshot();
expect(output.authConfig).toMatchSnapshot();
expect(getProjectSchema(projRoot, 'myApiName')).toMatchSnapshot();
});
});