Skip to content

Commit

Permalink
feat: generate datastore models for Admin CMS to consume post-deploym…
Browse files Browse the repository at this point in the history
…ent from CLI (#6771)

* feat: generate datastore models for Admin CMS to consume post-deployment from CLI
  • Loading branch information
kaustavghosh06 committed Mar 4, 2021
1 parent b53f256 commit 0e74b65
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 4 deletions.
5 changes: 2 additions & 3 deletions packages/amplify-e2e-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,15 @@ export async function createNewProjectDir(
projectName: string,
prefix = path.join(fs.realpathSync(os.tmpdir()), amplifyTestsDir),
): Promise<string> {
const currentHash = execSync('git rev-parse --short HEAD', { cwd: __dirname })
.toString()
.trim();
const currentHash = execSync('git rev-parse --short HEAD', { cwd: __dirname }).toString().trim();
let projectDir;
do {
const randomId = await global.getRandomId();
projectDir = path.join(prefix, `${projectName}_${currentHash}_${randomId}`);
} while (fs.existsSync(projectDir));

fs.ensureDirSync(projectDir);
console.log(projectDir);
return projectDir;
}

Expand Down
41 changes: 41 additions & 0 deletions packages/amplify-e2e-core/src/init/adminUI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { setupAmplifyAdminUI, getAmplifyBackendJobStatus } from '../utils/sdk-calls';

export async function enableAdminUI(appId: string, envName: string, region: string) {
const setupAdminUIJobDetails = await setupAmplifyAdminUI(appId, region);

const jobCompletionDetails = await pollUntilDone(setupAdminUIJobDetails.JobId, appId, envName, region, 2 * 1000, 2000 * 1000);

if (jobCompletionDetails.Status === 'FAILED') {
throw new Error('Setting up Admin UI failed');
}
}

// interval is how often to poll
// timeout is how long to poll waiting for a result (0 means try forever)

async function pollUntilDone(jobId: string, appId: string, envName: string, region: string, interval: number, timeout: number) {
const start = Date.now();
while (true) {
const jobDetails = await getAmplifyBackendJobStatus(jobId, appId, envName, region);

if (jobDetails.Status === 'FAILED' || jobDetails.Status === 'COMPLETED') {
// we know we're done here, return from here whatever you
// want the final resolved value of the promise to be
return jobDetails;
} else {
if (timeout !== 0 && Date.now() - start > timeout) {
throw new Error(`Job Timed out for ${jobId}`);
} else {
// run again with a short delay
await delay(interval);
}
}
}
}

// create a promise that resolves after a short delay
function delay(t: number) {
return new Promise(function (resolve) {
setTimeout(resolve, t);
});
}
1 change: 1 addition & 0 deletions packages/amplify-e2e-core/src/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './amplifyPush';
export * from './deleteProject';
export * from './initProjectHelper';
export * from './pull-headless';
export * from './adminUI';
6 changes: 6 additions & 0 deletions packages/amplify-e2e-core/src/utils/projectMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ function getBackendConfig(projectRoot: string) {
return JSON.parse(fs.readFileSync(backendFConfigFilePath, 'utf8'));
}

function getLocalEnvInfo(projectRoot: string) {
const localEnvInfoFilePath: string = path.join(projectRoot, 'amplify', '.config', 'local-env-info.json');
return JSON.parse(fs.readFileSync(localEnvInfoFilePath, 'utf8'));
}

function getCloudBackendConfig(projectRoot: string) {
const currentCloudPath: string = path.join(projectRoot, 'amplify', '#current-cloud-backend', 'backend-config.json');
return JSON.parse(fs.readFileSync(currentCloudPath, 'utf8'));
Expand Down Expand Up @@ -133,4 +138,5 @@ export {
getParameters,
getCloudBackendConfig,
setParameters,
getLocalEnvInfo,
};
19 changes: 19 additions & 0 deletions packages/amplify-e2e-core/src/utils/sdk-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
CloudWatchEvents,
Kinesis,
CloudFormation,
AmplifyBackend,
} from 'aws-sdk';
import _ from 'lodash';

Expand Down Expand Up @@ -226,3 +227,21 @@ export const getCloudWatchEventRule = async (targetName: string, region: string)
}
return ruleName;
};

export const setupAmplifyAdminUI = async (appId: string, region: string) => {
const amplifyBackend = new AmplifyBackend({ region });

return await amplifyBackend.createBackendConfig({ AppId: appId }).promise();
};

export const getAmplifyBackendJobStatus = async (jobId: string, appId: string, envName: string, region: string) => {
const amplifyBackend = new AmplifyBackend({ region });

return await amplifyBackend
.getBackendJob({
JobId: jobId,
AppId: appId,
BackendEnvironmentName: envName,
})
.promise();
};
35 changes: 34 additions & 1 deletion packages/amplify-e2e-tests/src/__tests__/api_2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as path from 'path';
import { existsSync } from 'fs';
import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync';
import gql from 'graphql-tag';
const providerName = 'awscloudformation';

import {
addApiWithSchema,
Expand All @@ -17,7 +18,9 @@ import {
deleteProjectDir,
getAppSyncApi,
getProjectMeta,
getLocalEnvInfo,
getTransformConfig,
enableAdminUI,
} from 'amplify-e2e-core';
import { TRANSFORM_CURRENT_VERSION } from 'graphql-transformer-core';
import _ from 'lodash';
Expand Down Expand Up @@ -48,7 +51,7 @@ describe('amplify add api (GraphQL)', () => {
await amplifyPush(projRoot);

const meta = getProjectMeta(projRoot);
const region = meta['providers']['awscloudformation']['Region'] as string;
const region = meta['providers'][providerName]['Region'] as string;
const { output } = meta.api[name];
const url = output.GraphQLAPIEndpointOutput as string;
const apiKey = output.GraphQLAPIKeyOutput as string;
Expand Down Expand Up @@ -127,6 +130,7 @@ describe('amplify add api (GraphQL)', () => {
const name = `conflictdetection`;
await initJSProjectWithProfile(projRoot, { name });
await addApiWithSchemaAndConflictDetection(projRoot, 'simple_model.graphql');

await amplifyPush(projRoot);

const meta = getProjectMeta(projRoot);
Expand Down Expand Up @@ -158,6 +162,35 @@ describe('amplify add api (GraphQL)', () => {
expect(_.isEmpty(disableDSConfig.ResolverConfig)).toBe(true);
});

it('init a project with conflict detection enabled and admin UI enabled to generate datastore models in the cloud', async () => {
const name = `dsadminui`;
await initJSProjectWithProfile(projRoot, { disableAmplifyAppCreation: false, name });

const meta = getProjectMeta(projRoot);
const appId = meta.providers?.[providerName]?.AmplifyAppId;
const region = meta.providers?.[providerName]?.Region;

const localEnvInfo = getLocalEnvInfo(projRoot);
const envName = localEnvInfo.envName;

// setupAdminUI
await enableAdminUI(appId, envName, region);

await addApiWithSchemaAndConflictDetection(projRoot, 'simple_model.graphql');
await amplifyPush(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);
});

it('init a sync enabled project and update conflict resolution strategy', async () => {
const name = `syncenabled`;
await initJSProjectWithProfile(projRoot, { name });
Expand Down
106 changes: 106 additions & 0 deletions packages/amplify-provider-awscloudformation/src/admin-modelgen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import ora from 'ora';
import AWS from 'aws-sdk';
import { $TSContext, stateManager, pathManager, $TSAny } from 'amplify-cli-core';
import { isDataStoreEnabled } from 'graphql-transformer-core';
import * as path from 'path';
import { ProviderName as providerName } from './constants';
import { isAmplifyAdminApp } from './utils/admin-helpers';
import { AmplifyBackend } from './aws-utils/aws-amplify-backend';

export async function adminModelgen(context: $TSContext, resources: $TSAny[]) {
const appSyncResources = resources.filter(resource => resource.service === 'AppSync');

if (appSyncResources.length === 0) {
return;
}
const appSyncResource = appSyncResources[0];
const { resourceName } = appSyncResource;

const amplifyMeta = stateManager.getMeta();
const localEnvInfo = stateManager.getLocalEnvInfo();

const appId = amplifyMeta?.providers?.[providerName]?.AmplifyAppId;
if (!appId) {
context.print.error('Could not find AmplifyAppId in amplify-meta.json.');
return;
}
const envName = localEnvInfo.envName;
const { isAdminApp } = await isAmplifyAdminApp(appId);
const isDSEnabled = await isDataStoreEnabled(path.join(pathManager.getBackendDirPath(), 'api', resourceName));

if (!isAdminApp || !isDSEnabled) {
return;
}
// Generate DataStore Models for Admin UI CMS to consume
const spinner = ora('Generating models in the cloud…\n').start();
const amplifyBackendInstance = await AmplifyBackend.getInstance(context);
try {
const jobStartDetails = await amplifyBackendInstance.amplifyBackend
.generateBackendAPIModels({
AppId: appId,
BackendEnvironmentName: envName,
ResourceName: resourceName,
})
.promise();

const jobCompletionDetails = await pollUntilDone(
jobStartDetails.JobId,
appId,
envName,
2 * 1000,
2000 * 1000,
amplifyBackendInstance.amplifyBackend,
);
if (jobCompletionDetails.Status === 'COMPLETED') {
spinner.succeed('Successfully generated models in the cloud.');
} else {
throw new Error('Modelgen job creation failed');
}
} catch (e) {
spinner.stop();
context.print.error(`Failed to create models in the cloud: ${e.message}`);
}
}

// interval is how often to poll
// timeout is how long to poll waiting for a result (0 means try forever)

async function pollUntilDone(
jobId: string,
appId: string,
backendEnvironmentName: string,
interval: number,
timeout: number,
amplifyBackendClient: AWS.AmplifyBackend,
) {
const start = Date.now();
while (true) {
const jobDetails = await amplifyBackendClient
.getBackendJob({
JobId: jobId,
AppId: appId,
BackendEnvironmentName: backendEnvironmentName,
})
.promise();

if (jobDetails.Status === 'FAILED' || jobDetails.Status === 'COMPLETED') {
// we know we're done here, return from here whatever you
// want the final resolved value of the promise to be
return jobDetails;
} else {
if (timeout !== 0 && Date.now() - start > timeout) {
throw new Error(`Job Timed out for ${jobId}`);
} else {
// run again with a short delay
await delay(interval);
}
}
}
}

// create a promise that resolves after a short delay
function delay(t: number) {
return new Promise(function (resolve) {
setTimeout(resolve, t);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import AWS from 'aws-sdk';
import aws from './aws';
import { loadConfiguration } from '../configuration-manager';
import { $TSContext } from 'amplify-cli-core';

export class AmplifyBackend {
private static instance: AmplifyBackend;
private readonly context: $TSContext;
public amplifyBackend: AWS.AmplifyBackend;

static async getInstance(context: $TSContext, options = {}): Promise<AmplifyBackend> {
if (!AmplifyBackend.instance) {
let cred = {};
try {
cred = await loadConfiguration(context);
} catch (e) {
// ignore missing config
}
AmplifyBackend.instance = new AmplifyBackend(context, cred, options);
}
return AmplifyBackend.instance;
}

private constructor(context: $TSContext, creds, options = {}) {
this.context = context;
this.amplifyBackend = new aws.AmplifyBackend({ ...creds, ...options });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { formUserAgentParam } from './aws-utils/user-agent';
import constants, { ProviderName as providerName } from './constants';
import { uploadAppSyncFiles } from './upload-appsync-files';
import { prePushGraphQLCodegen, postPushGraphQLCodegen } from './graphql-codegen';
import { adminModelgen } from './admin-modelgen';
import { prePushAuthTransform } from './auth-transform';
import { transformGraphQLSchema } from './transform-graphql-schema';
import { displayHelpfulURLs } from './display-helpful-urls';
Expand Down Expand Up @@ -311,6 +312,7 @@ export async function run(context: $TSContext, resourceDefinition: $TSObject) {
.filter(resource => resource.category === 'auth' && resource.service === 'Cognito' && resource.providerPlugin === 'awscloudformation')
.map(({ category, resourceName }) => context.amplify.removeDeploymentSecrets(context, category, resourceName));

await adminModelgen(context, resources);
spinner.succeed('All resources are updated in the cloud');

await displayHelpfulURLs(context, resources);
Expand Down

0 comments on commit 0e74b65

Please sign in to comment.