Skip to content

Commit

Permalink
fix: headless api migration (#8992)
Browse files Browse the repository at this point in the history
  • Loading branch information
akshbhu committed Nov 21, 2021
1 parent cf42a3b commit e1b4cf7
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 10 deletions.
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));
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) {
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();
});
});

0 comments on commit e1b4cf7

Please sign in to comment.