Skip to content

Commit

Permalink
Add form cli (#255)
Browse files Browse the repository at this point in the history
* feat: add form cli

---------

Co-authored-by: Edward Foyle <[email protected]>
Co-authored-by: Amplifiyer <[email protected]>
  • Loading branch information
3 people authored and Abhishek Raj committed Sep 27, 2023
1 parent d5aba32 commit 6dc935f
Show file tree
Hide file tree
Showing 15 changed files with 11,571 additions and 13,165 deletions.
5 changes: 5 additions & 0 deletions .changeset/moody-melons-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/backend-cli': minor
---

Add form generation interface
3 changes: 3 additions & 0 deletions examples/simple_react/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ yarn-error.log*

cdk.out
src/amplifyconfiguration.js
src/ui-components/
src/graphql/

24,267 changes: 11,142 additions & 13,125 deletions examples/simple_react/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion examples/simple_react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"@aws-amplify/backend": "file:../../packages/backend",
"@aws-amplify/backend-auth": "file:../../packages/backend-auth",
"@aws-amplify/backend-graphql": "file:../../packages/backend-graphql",
"@aws-amplify/cli": "file:../../packages/cli"
"@aws-amplify/cli": "file:../../packages/cli",
"@aws-amplify/api": "^5.4.5"
},
"scripts": {
"start": "react-scripts start",
Expand Down
42 changes: 15 additions & 27 deletions examples/simple_react/src/components/todos/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ import {
Divider,
TextField,
} from '@aws-amplify/ui-react';
import { GraphQLQuery } from '@aws-amplify/api';
import { BiTrash } from 'react-icons/bi';
import { Todo } from '../../models';
import style from './style';
import { listTodos } from '../../graphql/queries';
import { API } from 'aws-amplify';
import { deleteTodo } from '../../graphql/mutations';
import { TodoCreateForm } from '../../ui-components';

const initialState = { name: '', description: '' };

Expand All @@ -26,8 +31,14 @@ function TodoView() {

const fetchTodos = async () => {
try {
const todos = await DataStore.query(Todo);
setTodos(todos);
const {
data: {
listTodos: { items },
},
} = await API.graphql<GraphQLQuery<any>>({
query: listTodos,
});
setTodos(items);
} catch (error) {
console.log('error fetching todos:', error);
}
Expand All @@ -51,10 +62,7 @@ function TodoView() {

const removeTodo = async (id: string) => {
try {
const toDelete = await DataStore.query(Todo, id);
if (toDelete) {
await DataStore.delete(toDelete);
}
await API.graphql({ query: deleteTodo, variables: { input: { id } } });
fetchTodos();
} catch (error) {
console.log('error removing todo:', error);
Expand Down Expand Up @@ -87,27 +95,7 @@ function TodoView() {
</Card>
)}
</Collection>
<View style={style.form}>
<TextField
labelHidden
label="Name"
placeholder="Name"
style={style.form.textInput}
value={formState.name}
onChange={(event) => setInput('name', event.target.value)}
/>
<TextField
labelHidden
label="Description"
placeholder="Description"
style={style.form.textInput}
value={formState.description}
onChange={(event) => setInput('description', event.target.value)}
/>
</View>
<Button variation="primary" onClick={addTodo}>
Create Todo
</Button>
<TodoCreateForm onSuccess={() => fetchTodos()} />
</Card>
);
}
Expand Down
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
},
"homepage": "https://github.com/aws-amplify/cli#readme",
"dependencies": {
"@aws-amplify/backend-output-schemas": "^0.2.0-alpha.3",
"@aws-amplify/backend-secret": "^0.2.0-alpha.0",
"@aws-amplify/client-config": "^0.2.0-alpha.6",
"@aws-amplify/deployed-backend-client": "^0.2.0-alpha.0",
"@aws-amplify/form-generator": "^0.2.0-alpha.1",
"@aws-amplify/model-generator": "^0.2.0-alpha.2",
"@aws-amplify/sandbox": "^0.2.0-alpha.6",
"@aws-sdk/credential-providers": "^3.360.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ export class GenerateConfigCommand
*/
readonly describe: string;

private readonly missingArgsError = new Error(
'Either --stack or --branch must be provided'
);

/**
* Creates client config generation command.
*/
Expand Down Expand Up @@ -100,7 +96,7 @@ export class GenerateConfigCommand
})
.check((argv) => {
if (!argv.stack && !argv.branch) {
throw this.missingArgsError;
throw new Error('Either --stack or --branch must be provided');
}
return true;
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import path from 'path';
import { createLocalGraphqlFormGenerator } from '@aws-amplify/form-generator';
import { createGraphqlDocumentGenerator } from '@aws-amplify/model-generator';
import { BackendIdentifier } from '@aws-amplify/deployed-backend-client';
import { AwsCredentialIdentityProvider } from '@aws-sdk/types';

type FormGenerationParams = {
modelsOutDir: string;
uiOutDir: string;
apiUrl: string;
backendIdentifier: BackendIdentifier;
};
type FormGenerationInstanceOptions = {
credentialProvider: AwsCredentialIdentityProvider;
};
/**
* Creates a handler for FormGeneration
*/
export class FormGenerationHandler {
/**
* Instantiates the handler
*/
constructor(private readonly formGenParams: FormGenerationInstanceOptions) {}
generate = async (params: FormGenerationParams) => {
const { backendIdentifier, modelsOutDir, uiOutDir, apiUrl } = params;
const { credentialProvider } = this.formGenParams;
const graphqlClientGenerator = createGraphqlDocumentGenerator({
backendIdentifier,
credentialProvider,
});
const modelsResult = await graphqlClientGenerator.generateModels({
language: 'typescript',
});
await modelsResult.writeToDirectory(modelsOutDir);
const relativePath = path.relative(uiOutDir, modelsOutDir);
const localFormGenerator = createLocalGraphqlFormGenerator({
introspectionSchemaUrl: apiUrl,
graphqlModelDirectoryPath: relativePath,
});
const result = await localFormGenerator.generateForms();
await result.writeToDirectory(uiOutDir);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { graphqlOutputKey } from '@aws-amplify/backend-output-schemas';
import { BackendOutputClient } from '@aws-amplify/deployed-backend-client';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import assert from 'node:assert';
import { describe, it, mock } from 'node:test';
import yargs, { CommandModule } from 'yargs';
import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
import { TestCommandRunner } from '../../../test-utils/command_runner.js';
import { FormGenerationHandler } from './form_generation_handler.js';
import { GenerateFormsCommand } from './generate_forms_command.js';

void describe('generate forms command', () => {
void describe('form generation validation', () => {
void it('modelsOutDir path can be customized', async () => {
const credentialProvider = fromNodeProviderChain();

const backendIdResolver = new BackendIdentifierResolver({
resolve: () => Promise.resolve('testAppName'),
});
const formGenerationHandler = new FormGenerationHandler({
credentialProvider,
});

const fakedBackendOutputClient = new BackendOutputClient(
credentialProvider,
{ stackName: 'test_stack' }
);

const generateFormsCommand = new GenerateFormsCommand(
backendIdResolver,
() => fakedBackendOutputClient,
formGenerationHandler
);

const generationMock = mock.method(formGenerationHandler, 'generate');
generationMock.mock.mockImplementation(async () => undefined);
mock
.method(fakedBackendOutputClient, 'getOutput')
.mock.mockImplementation(async () => ({
[graphqlOutputKey]: {
payload: {
awsAppsyncApiId: 'test_api_id',
amplifyApiModelSchemaS3Uri: 'test_schema',
awsAppsyncApiEndpoint: 'test_endpoint',
},
},
}));
const parser = yargs().command(
generateFormsCommand as unknown as CommandModule
);

const modelsOutPath = './my-fake-models-path';
const commandRunner = new TestCommandRunner(parser);
await commandRunner.runCommand(
`forms --stack my_stack --modelsOutDir ${modelsOutPath}`
);
assert.equal(
generationMock.mock.calls[0].arguments[0].modelsOutDir,
modelsOutPath
);
});
void it('uiOutDir path can be customized', async () => {
const credentialProvider = fromNodeProviderChain();

const backendIdResolver = new BackendIdentifierResolver({
resolve: () => Promise.resolve('testAppName'),
});
const formGenerationHandler = new FormGenerationHandler({
credentialProvider,
});

const fakedBackendOutputClient = new BackendOutputClient(
credentialProvider,
{ stackName: 'test_stack' }
);

const generateFormsCommand = new GenerateFormsCommand(
backendIdResolver,
() => fakedBackendOutputClient,
formGenerationHandler
);

const generationMock = mock.method(formGenerationHandler, 'generate');
generationMock.mock.mockImplementation(async () => undefined);
mock
.method(fakedBackendOutputClient, 'getOutput')
.mock.mockImplementation(async () => ({
[graphqlOutputKey]: {
payload: {
awsAppsyncApiId: 'test_api_id',
amplifyApiModelSchemaS3Uri: 'test_schema',
awsAppsyncApiEndpoint: 'test_endpoint',
},
},
}));
const parser = yargs().command(
generateFormsCommand as unknown as CommandModule
);

const uiOutPath = './my-fake-ui-path';
const commandRunner = new TestCommandRunner(parser);
await commandRunner.runCommand(
`forms --stack my_stack --uiOutDir ${uiOutPath}`
);
assert.equal(
generationMock.mock.calls[0].arguments[0].uiOutDir,
uiOutPath
);
});
void it('./src/ui-components is the default graphql model generation path', async () => {
const credentialProvider = fromNodeProviderChain();

const backendIdResolver = new BackendIdentifierResolver({
resolve: () => Promise.resolve('testAppName'),
});
const formGenerationHandler = new FormGenerationHandler({
credentialProvider,
});

const fakedBackendOutputClient = new BackendOutputClient(
credentialProvider,
{ stackName: 'test_stack' }
);

const generateFormsCommand = new GenerateFormsCommand(
backendIdResolver,
() => fakedBackendOutputClient,
formGenerationHandler
);

const generationMock = mock.method(formGenerationHandler, 'generate');
generationMock.mock.mockImplementation(async () => undefined);
mock
.method(fakedBackendOutputClient, 'getOutput')
.mock.mockImplementation(async () => ({
[graphqlOutputKey]: {
payload: {
awsAppsyncApiId: 'test_api_id',
amplifyApiModelSchemaS3Uri: 'test_schema',
awsAppsyncApiEndpoint: 'test_endpoint',
},
},
}));
const parser = yargs().command(
generateFormsCommand as unknown as CommandModule
);
const commandRunner = new TestCommandRunner(parser);
await commandRunner.runCommand('forms --stack my_stack');
assert.equal(
generationMock.mock.calls[0].arguments[0].uiOutDir,
'./src/ui-components'
);
});
void it('./src/graphql is the default graphql model generation path', async () => {
const credentialProvider = fromNodeProviderChain();

const backendIdResolver = new BackendIdentifierResolver({
resolve: () => Promise.resolve('testAppName'),
});
const formGenerationHandler = new FormGenerationHandler({
credentialProvider,
});

const fakedBackendOutputClient = new BackendOutputClient(
credentialProvider,
{ stackName: 'test_stack' }
);

const generateFormsCommand = new GenerateFormsCommand(
backendIdResolver,
() => fakedBackendOutputClient,
formGenerationHandler
);

const generationMock = mock.method(formGenerationHandler, 'generate');
generationMock.mock.mockImplementation(async () => undefined);
mock
.method(fakedBackendOutputClient, 'getOutput')
.mock.mockImplementation(async () => ({
[graphqlOutputKey]: {
payload: {
awsAppsyncApiId: 'test_api_id',
amplifyApiModelSchemaS3Uri: 'test_schema',
awsAppsyncApiEndpoint: 'test_endpoint',
},
},
}));
const parser = yargs().command(
generateFormsCommand as unknown as CommandModule
);
const commandRunner = new TestCommandRunner(parser);
await commandRunner.runCommand('forms --stack my_stack');
assert.equal(
generationMock.mock.calls[0].arguments[0].modelsOutDir,
'./src/graphql'
);
});
});
});
Loading

0 comments on commit 6dc935f

Please sign in to comment.