Skip to content

Commit

Permalink
fix: add in custom vtl transformer (#284)
Browse files Browse the repository at this point in the history
  • Loading branch information
kcwinner authored and Ken Winner committed Sep 18, 2021
1 parent 1c5906e commit f19a0ac
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 18 deletions.
8 changes: 8 additions & 0 deletions .eslintrc.json

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

9 changes: 9 additions & 0 deletions .projenrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,13 @@ const project = new AwsCdkConstructLibrary({
const unbumpTask = project.tasks.tryFind('unbump');
unbumpTask.exec('git checkout package-lock.json');

project.eslint.overrides.push({
files: [
'custom-vtl-transformer.ts',
],
rules: {
'import/no-extraneous-dependencies': 'off',
},
});

project.synth();
2 changes: 2 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ new AppSyncTransformer(scope: Construct, id: string, props: AppSyncTransformerPr
* **schemaPath** (<code>string</code>) Relative path where schema.graphql exists.
* **apiName** (<code>string</code>) String value representing the api name. __*Default*__: `${id}-api`
* **authorizationConfig** (<code>[AuthorizationConfig](#aws-cdk-aws-appsync-authorizationconfig)</code>) Optional. __*Default*__: API_KEY authorization config
* **customVtlTransformerRootDirectory** (<code>string</code>) The root directory to use for finding custom resolvers. __*Default*__: process.cwd()
* **dynamoDbStreamConfig** (<code>Map<string, [StreamViewType](#aws-cdk-aws-dynamodb-streamviewtype)></code>) A map of @model type names to stream view type e.g { Blog: StreamViewType.NEW_IMAGE }. __*Optional*__
* **enableDynamoPointInTimeRecovery** (<code>boolean</code>) Whether to enable dynamo Point In Time Recovery. __*Default*__: false
* **fieldLogLevel** (<code>[FieldLogLevel](#aws-cdk-aws-appsync-fieldloglevel)</code>) Optional. __*Default*__: FieldLogLevel.NONE
Expand Down Expand Up @@ -155,6 +156,7 @@ Name | Type | Description
**schemaPath**🔹 | <code>string</code> | Relative path where schema.graphql exists.
**apiName**?🔹 | <code>string</code> | String value representing the api name.<br/>__*Default*__: `${id}-api`
**authorizationConfig**?🔹 | <code>[AuthorizationConfig](#aws-cdk-aws-appsync-authorizationconfig)</code> | Optional.<br/>__*Default*__: API_KEY authorization config
**customVtlTransformerRootDirectory**?🔹 | <code>string</code> | The root directory to use for finding custom resolvers.<br/>__*Default*__: process.cwd()
**dynamoDbStreamConfig**?🔹 | <code>Map<string, [StreamViewType](#aws-cdk-aws-dynamodb-streamviewtype)></code> | A map of @model type names to stream view type e.g { Blog: StreamViewType.NEW_IMAGE }.<br/>__*Optional*__
**enableDynamoPointInTimeRecovery**?🔹 | <code>boolean</code> | Whether to enable dynamo Point In Time Recovery.<br/>__*Default*__: false
**fieldLogLevel**?🔹 | <code>[FieldLogLevel](#aws-cdk-aws-appsync-fieldloglevel)</code> | Optional.<br/>__*Default*__: FieldLogLevel.NONE
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,26 @@ new AppSyncTransformer(this, "my-cool-api", {
});
```

#### Custom VTL Transformer

Can be used to create custom [NONE](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference-none.html) datasource resolvers.This allows for custom or special logic to be used and added via a transformer.

Example:

```graphql
type Thing {
fooBar: String
}

type Query {
listThingCustom: Thing
@custom(request: "test/custom-resolvers/Test/request.vtl", response: "test/custom-resolvers/Test/response.vtl")
}
```

The above will generate a `Query.listThingCustom` request and response resolver.
You can customize the location of custom resolvers using the `customVtlTransformerRootDirectory` property.

### Authentication

User Pool Authentication
Expand Down Expand Up @@ -380,3 +400,8 @@ Distributed under [Apache License, Version 2.0](LICENSE)
- [aws cdk](https://aws.amazon.com/cdk)
- [amplify-cli](https://github.com/aws-amplify/amplify-cli)
- [Amplify Directives](https://docs.amplify.aws/cli/graphql-transformer/directives)


# Sponsors

## [Stedi](https://www.stedi.com/)
7 changes: 7 additions & 0 deletions src/appsync-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ export interface AppSyncTransformerProps {
*/
readonly nestedStackName?: string;

/**
* The root directory to use for finding custom resolvers
* @default process.cwd()
*/
readonly customVtlTransformerRootDirectory?: string;

/**
* Optional. Additonal custom transformers to run prior to the CDK resource generations.
* Particularly useful for custom directives.
Expand Down Expand Up @@ -192,6 +198,7 @@ export class AppSyncTransformer extends Construct {
const transformerConfiguration: SchemaTransformerProps = {
schemaPath: props.schemaPath,
syncEnabled: props.syncEnabled ?? false,
customVtlTransformerRootDirectory: props.customVtlTransformerRootDirectory,
};

// Combine the arrays so we only loop once
Expand Down
136 changes: 136 additions & 0 deletions src/transformer/custom-vtl-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import * as fs from 'fs';
import * as path from 'path';
import { Fn, AppSync, IntrinsicFunction } from 'cloudform-types';
import Resolver from 'cloudform-types/types/appSync/resolver';
import {
FieldDefinitionNode,
DirectiveNode,
ObjectTypeDefinitionNode,
InterfaceTypeDefinitionNode,
Kind,
} from 'graphql';
import {
getDirectiveArgument,
ResolverResourceIDs,
ResourceConstants,
} from 'graphql-transformer-common';
import { Transformer, gql, TransformerContext } from 'graphql-transformer-core';

const CUSTOM_DIRECTIVE_STACK_NAME = 'CustomDirectiveStack';

/**
* Create a get item resolver for singular connections.
* @param type The parent type name.
* @param field The connection field name.
*/
/*eslint-disable @typescript-eslint/no-explicit-any */
function makeResolver(
type: string,
field: string,
request: string,
response: string,
datasourceName: string | IntrinsicFunction = 'NONE',
): Resolver {
return new Resolver({
ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'),
DataSourceName: datasourceName,
FieldName: field,
TypeName: type,
RequestMappingTemplate: request,
ResponseMappingTemplate: response,
}).dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID);
}

function noneDataSource() {
return new AppSync.DataSource({
ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'),
Name: 'NONE',
Type: 'NONE',
});
}

export class CustomVTLTransformer extends Transformer {
readonly rootDirectory: string;

constructor(rootDirectory: string) {
super(
'CustomVTLTransformer',
gql`
directive @custom(request: String, response: String) on FIELD_DEFINITION
`,
);

this.rootDirectory = rootDirectory;
}

public before = (acc: TransformerContext): void => {
const directiveList: DirectiveNode[] = [];

// gather all the http directives
for (const def of acc.inputDocument.definitions) {
if (def.kind === Kind.OBJECT_TYPE_DEFINITION && def.fields) {
for (const field of def.fields) {
if (field.directives) {
const customDirective = field.directives.find(
(dir: { name: { value: string } }) => dir.name.value === 'custom',
);
if (customDirective) {
directiveList.push(customDirective);
}
}
}
}
}
};

/*eslint-disable @typescript-eslint/no-explicit-any */
public field = (
parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode,
field: FieldDefinitionNode,
directive: DirectiveNode,
acc: TransformerContext,
): void => {
const parentTypeName = parent.name.value;
const fieldName = field.name.value;

// add none ds if that does not exist
const noneDS = acc.getResource(ResourceConstants.RESOURCES.NoneDataSource);
if (!noneDS) {
acc.setResource(ResourceConstants.RESOURCES.NoneDataSource, noneDataSource());
}

acc.mapResourceToStack(
CUSTOM_DIRECTIVE_STACK_NAME,
ResolverResourceIDs.ResolverResourceID(parentTypeName, fieldName),
);

const requestFile = getDirectiveArgument(directive, 'request');
const responseFile = getDirectiveArgument(directive, 'response');

let datasourceName: IntrinsicFunction | string = 'NONE';

if (!requestFile) {
throw new Error(
`The @custom directive on Type: ${parent.name.value} Field: ${field.name.value} is missing the request argument.`,
);
}

if (!responseFile) {
throw new Error(
`The @custom directive on Type: ${parent.name.value} Field: ${field.name.value} is missing the response argument.`,
);
}

let request, response;
try {
request = fs.readFileSync(path.join(this.rootDirectory, requestFile)).toString();
response = fs.readFileSync(path.join(this.rootDirectory, responseFile)).toString();
} catch (err) {
throw new Error(`Couldn't load VTL files. ${(err as Error).message}`);
}
const fieldMappingResolver = makeResolver(parentTypeName, fieldName, request, response, datasourceName);
acc.setResource(ResolverResourceIDs.ResolverResourceID(parentTypeName, fieldName), fieldMappingResolver);
const templateResources = acc.template.Resources;
if (!templateResources) return;
};
}
31 changes: 13 additions & 18 deletions src/transformer/schema-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
CdkTransformerFunctionResolver,
CdkTransformerHttpResolver,
} from './cdk-transformer';
import { CustomVTLTransformer } from './custom-vtl-transformer';

// Rebuilt this from cloudform-types because it has type errors
import { Resource } from './resource';
Expand Down Expand Up @@ -56,6 +57,12 @@ export interface SchemaTransformerProps {
* @default false
*/
readonly syncEnabled?: boolean;

/**
* The root directory to use for finding custom resolvers
* @default process.cwd()
*/
readonly customVtlTransformerRootDirectory?: string;
}

export interface SchemaTransformerOutputs {
Expand All @@ -72,6 +79,7 @@ export class SchemaTransformer {
public readonly schemaPath: string
public readonly outputPath: string
public readonly isSyncEnabled: boolean
public readonly customVtlTransformerRootDirectory: string;

private readonly authTransformerConfig: ModelAuthTransformerConfig

Expand All @@ -81,9 +89,10 @@ export class SchemaTransformer {
unauthRolePolicy: Resource | undefined

constructor(props: SchemaTransformerProps) {
this.schemaPath = props.schemaPath || './schema.graphql';
this.outputPath = props.outputPath || './appsync';
this.isSyncEnabled = props.syncEnabled || false;
this.schemaPath = props.schemaPath ?? './schema.graphql';
this.outputPath = props.outputPath ?? './appsync';
this.isSyncEnabled = props.syncEnabled ?? false;
this.customVtlTransformerRootDirectory = props.customVtlTransformerRootDirectory ?? process.cwd();

this.outputs = {};
this.resolvers = {};
Expand Down Expand Up @@ -125,21 +134,6 @@ export class SchemaTransformer {

const provider = new TransformerFeatureFlagProvider();

// const featureFlags = {
// getBoolean: (name: string, defaultValue: boolean) => {
// if (name === 'improvePluralization') {
// return true;
// }
// if (name === 'validateTypeNameReservedWords') {
// return false;
// }
// return defaultValue;
// },
// getString: (featureName: string, options?: string) => { return options },
// getNumber: (featureName: string, options?: number) => { return options },
// getObject: () => {},
// };

// Note: This is not exact as we are omitting the @searchable transformer as well as some others.
const transformer = new GraphQLTransform({
transformConfig: transformConfig,
Expand All @@ -153,6 +147,7 @@ export class SchemaTransformer {
new ModelConnectionTransformer(),
new ModelAuthTransformer(this.authTransformerConfig),
new HttpTransformer(),
new CustomVTLTransformer(this.customVtlTransformerRootDirectory),
...preCdkTransformers,
new CdkTransformer(),
...postCdkTransformers,
Expand Down
3 changes: 3 additions & 0 deletions test/custom-resolvers/Test/request.vtl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"version": "2018-05-29"
}
1 change: 1 addition & 0 deletions test/custom-resolvers/Test/response.vtl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$util.toJson({})
10 changes: 10 additions & 0 deletions test/customVtlTransformerSchema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Demonstrate custom vtl transform

type Thing {
fooBar: String
}

type Query {
listThingCustom: Thing
@custom(request: "test/custom-resolvers/Test/request.vtl", response: "test/custom-resolvers/Test/response.vtl")
}
45 changes: 45 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1133,4 +1133,49 @@ test('Custom Table Names Are Applied', () => {
{ AttributeName: 'productID', KeyType: 'RANGE' },
],
});
});

const customVtlTestSchemaPath = path.join(__dirname, 'customVtlTransformerSchema.graphql');

test('Custom VTL Transformer Creates Resolvers', () => {
const mockApp = new App();
const stack = new Stack(mockApp, 'custom-vtl-stack');

const appsyncTransformer = new AppSyncTransformer(stack, 'custom-vtl-transformer', {
schemaPath: customVtlTestSchemaPath,
authorizationConfig: apiKeyAuthorizationConfig,
xrayEnabled: false,
});

expect(appsyncTransformer.resolvers).toMatchObject({
QuerylistThingCustom: {
typeName: 'Query',
fieldName: 'listThingCustom',
requestMappingTemplate: 'appsync/resolvers/Query.listThingCustom.req',
responseMappingTemplate: 'appsync/resolvers/Query.listThingCustom.res',
},
});

expect(appsyncTransformer.nestedAppsyncStack).toHaveResourceLike('AWS::AppSync::Resolver', {
FieldName: 'listThingCustom',
TypeName: 'Query',
DataSourceName: 'NONE',
Kind: 'UNIT',
RequestMappingTemplate: '{\n "version": "2018-05-29"\n}',
ResponseMappingTemplate: '$util.toJson({})',
});
});

test('Can Set Custom Directory', () => {
const mockApp = new App();
const stack = new Stack(mockApp, 'custom-vtl-stack');

const customDir = path.join(process.cwd(), '..', 'cdk-appsync-transformer');

new AppSyncTransformer(stack, 'custom-vtl-transformer', {
schemaPath: customVtlTestSchemaPath,
authorizationConfig: apiKeyAuthorizationConfig,
xrayEnabled: false,
customVtlTransformerRootDirectory: customDir,
});
});

0 comments on commit f19a0ac

Please sign in to comment.