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

Gql preprocess #20

Merged
merged 16 commits into from
Jun 13, 2022
Merged
Show file tree
Hide file tree
Changes from 7 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
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { TransformerPluginBase, InvalidDirectiveError } from '@aws-amplify/graphql-transformer-core';
import {
TransformerContextProvider,
TransformerPluginType,
TransformerPluginType, TransformerPreProcessContextProvider,
TransformerSchemaVisitStepContextProvider,
} from '@aws-amplify/graphql-transformer-interfaces';
import { ObjectTypeDefinitionNode, DirectiveNode, Kind, DefinitionNode } from 'graphql';
import { ObjectTypeDefinitionNode, DirectiveNode, Kind, DefinitionNode, DocumentNode, ObjectTypeExtensionNode } from 'graphql';
import { createMappingLambda } from './field-mapping-lambda';
import { attachFilterAndConditionInputMappingSlot, attachInputMappingSlot, attachResponseMappingSlot } from './field-mapping-resolvers';

Expand All @@ -23,32 +23,25 @@ export class MapsToTransformer extends TransformerPluginBase {
* During the AST tree walking, the mapsTo transformer registers any renamed models with the ctx.resourceHelper
*/
object = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerSchemaVisitStepContextProvider) => {
const modelName = definition.name.value;
const prevNameNode = directive.arguments?.find(arg => arg.name.value === 'name');

const hasModelDirective = !!definition.directives?.find(directive => directive.name.value === 'model');
if (!hasModelDirective) {
throw new InvalidDirectiveError(`@mapsTo can only be used on an @model type`);
}

// the following checks should never fail because the graphql schema already validates them, but TS complains without them
if (!prevNameNode) {
throw new InvalidDirectiveError(`name is required in @${directiveName} directive`);
}

if (prevNameNode.value.kind !== 'StringValue') {
throw new InvalidDirectiveError(`A single string must be provided for "name" in @${directiveName} directive`);
}

const originalName = prevNameNode.value.value;

const schemaHasConflictingModel = !!ctx.inputDocument.definitions.find(hasModelWithNamePredicate(originalName));
if (schemaHasConflictingModel) {
throw new InvalidDirectiveError(`Type ${modelName} cannot map to ${originalName} because ${originalName} is a model in the schema.`);
}
ctx.resourceHelper.setModelNameMapping(modelName, originalName);
updateTypeMapping(definition, directive, ctx.inputDocument, ctx.resourceHelper.setModelNameMapping);
};

/**
* Run pre-mutation steps on the schema to support mapsTo
* @param context The pre-processing context for the transformer, used to store type mappings
*/
preMutateSchema = (context: TransformerPreProcessContextProvider) => {
context.inputDocument?.definitions?.forEach(def => {
if (def.kind === 'ObjectTypeDefinition' || def.kind === 'ObjectTypeExtension') {
def?.directives?.forEach(dir => {
if (dir.name.value === directiveName) {
updateTypeMapping(def, dir, context.inputDocument, context.schemaHelper.setTypeMapping);
}
});
}
});
}

/**
* During the generateResolvers step, the mapsTo transformer reads all of the model field mappings from the resourceHelper and generates
* VTL to map the current field names to the original field names
Expand Down Expand Up @@ -111,6 +104,38 @@ export class MapsToTransformer extends TransformerPluginBase {
};
}

const updateTypeMapping = (
definition: ObjectTypeDefinitionNode | ObjectTypeExtensionNode,
directive: DirectiveNode,
inputDocument: DocumentNode,
updateFunction: (newTypeName: string, originalTypeName:string) => void,
) => {
const modelName = definition.name.value;
const prevNameNode = directive.arguments?.find(arg => arg.name.value === 'name');

const hasModelDirective = !!definition.directives?.find(directive => directive.name.value === 'model');
if (!hasModelDirective) {
throw new InvalidDirectiveError(`@mapsTo can only be used on an @model type`);
}

// the following checks should never fail because the graphql schema already validates them, but TS complains without them
if (!prevNameNode) {
throw new InvalidDirectiveError(`name is required in @${directiveName} directive`);
}

if (prevNameNode.value.kind !== 'StringValue') {
throw new InvalidDirectiveError(`A single string must be provided for "name" in @${directiveName} directive`);
}

const originalName = prevNameNode.value.value;

const schemaHasConflictingModel = !!inputDocument.definitions.find(hasModelWithNamePredicate(originalName));
if (schemaHasConflictingModel) {
throw new InvalidDirectiveError(`Type ${modelName} cannot map to ${originalName} because ${originalName} is a model in the schema.`);
}
updateFunction(modelName, originalName);
}

// returns a predicate for determining if a DefinitionNode is an model object with the given name
const hasModelWithNamePredicate = (name: string) => (node: DefinitionNode) =>
node.kind === Kind.OBJECT_TYPE_DEFINITION && !!node.directives?.find(dir => dir.name.value === 'model') && node.name.value === name;
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"@aws-cdk/core": "~1.124.0",
"graphql": "^14.5.8",
"graphql-mapping-template": "4.20.3",
"graphql-transformer-common": "4.23.0"
"graphql-transformer-common": "4.23.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

this line is a dup of 41

"immer": "^9.0.12"
},
"jest": {
"transform": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IndexTransformer, PrimaryKeyTransformer } from '@aws-amplify/graphql-index-transformer';
import { ModelTransformer } from '@aws-amplify/graphql-model-transformer';
import { GraphQLTransform, validateModelSchema } from '@aws-amplify/graphql-transformer-core';
import { Kind, parse } from 'graphql';
import { DocumentNode, Kind, parse } from 'graphql';
import { BelongsToTransformer, HasManyTransformer, HasOneTransformer } from '..';

test('fails if @belongsTo was used on an object that is not a model type', () => {
Expand Down Expand Up @@ -364,3 +364,64 @@ test('support for belongs to with Int fields', () => {
'$util.defaultIfNullOrBlank($ctx.source.owningBankId, "___xamznone____"))',
);
});

describe('Pre Processing Belongs To Tests', () => {
let transformer: GraphQLTransform;
const hasGeneratedField = (doc: DocumentNode, objectType: string, fieldName: string): boolean => {
let hasField = false;
let doubleHasField = false;
doc?.definitions?.forEach(def => {
if ((def.kind === 'ObjectTypeDefinition' || def.kind === 'ObjectTypeExtension') && def.name.value === objectType) {
def?.fields?.forEach(field => {
if (hasField && field.name.value === fieldName) {
doubleHasField = true;
} else if (field.name.value === fieldName) {
hasField = true;
}
});
}
});
return doubleHasField ? false : hasField;
};

beforeEach(() => {
transformer = new GraphQLTransform({
transformers: [new ModelTransformer(), new HasManyTransformer(), new HasOneTransformer(), new BelongsToTransformer()],
});
});

test('Should generate connecting field for a has one', () => {
const schema = `
type Blog @model {
id: ID!
postsField: Post @hasOne
}

type Post @model {
id: ID!
blogField: Blog @belongsTo
}
`;

const updatedSchemaDoc = transformer.preProcessSchema(parse(schema));
expect(hasGeneratedField(updatedSchemaDoc, 'Post', 'postBlogFieldId')).toBeTruthy();
});

test('Should not generate extra connecting field for a has many, there\'s already a route back to parent', () => {
const schema = `
type Blog @model {
id: ID!
postsField: [Post] @hasMany
}

type Post @model {
id: ID!
blogField: Blog @belongsTo
}
`;

const updatedSchemaDoc = transformer.preProcessSchema(parse(schema));
expect(hasGeneratedField(updatedSchemaDoc, 'Post', 'postBlogFieldId')).toBeFalsy();
expect(hasGeneratedField(updatedSchemaDoc, 'Post', 'blogPostsFieldId')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IndexTransformer, PrimaryKeyTransformer } from '@aws-amplify/graphql-index-transformer';
import { ModelTransformer } from '@aws-amplify/graphql-model-transformer';
import { ConflictHandlerType, GraphQLTransform, validateModelSchema } from '@aws-amplify/graphql-transformer-core';
import { Kind, parse } from 'graphql';
import { DocumentNode, Kind, parse } from 'graphql';
import { BelongsToTransformer, HasManyTransformer, HasOneTransformer } from '..';

test('fails if used as a has one relation', () => {
Expand Down Expand Up @@ -778,3 +778,42 @@ test('has many with queries null generate correct filter input objects for enum
expect(statusField).toBeDefined();
expect(statusField.type.name.value).toMatch('ModelIssueStatusInput');
});

describe('Pre Processing Has Many Tests', () => {
let transformer: GraphQLTransform;
const hasGeneratedField = (doc: DocumentNode, objectType: string, fieldName: string): boolean => {
let hasField = false;
doc?.definitions?.forEach(def => {
if ((def.kind === 'ObjectTypeDefinition' || def.kind === 'ObjectTypeExtension') && def.name.value === objectType) {
def?.fields?.forEach(field => {
if (field.name.value === fieldName) {
hasField = true;
}
});
}
});
return hasField;
};

beforeEach(() => {
transformer = new GraphQLTransform({
transformers: [new ModelTransformer(), new HasManyTransformer()],
});
});

test('Should generate connecting field when one is not provided', () => {
const schema = `
type Blog @model {
id: ID!
postsField: [Post] @hasMany
}

type Post @model {
id: ID!
}
`;

const updatedSchemaDoc = transformer.preProcessSchema(parse(schema));
expect(hasGeneratedField(updatedSchemaDoc, 'Post', 'blogPostsFieldId')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PrimaryKeyTransformer } from '@aws-amplify/graphql-index-transformer';
import { ModelTransformer } from '@aws-amplify/graphql-model-transformer';
import { ConflictHandlerType, GraphQLTransform, validateModelSchema } from '@aws-amplify/graphql-transformer-core';
import { Kind, parse } from 'graphql';
import { DocumentNode, Kind, parse } from 'graphql';
import { HasManyTransformer, HasOneTransformer } from '..';

test('fails if @hasOne was used on an object that is not a model type', () => {
Expand Down Expand Up @@ -466,3 +466,123 @@ test('recursive @hasOne relationships are supported if DataStore is enabled', ()
const schema = parse(out.schema);
validateModelSchema(schema);
});

describe('Pre Processing Has One Tests', () => {
let transformer: GraphQLTransform;
const hasGeneratedField = (doc: DocumentNode, objectType: string, fieldName: string): boolean => {
let hasField = false;
doc?.definitions?.forEach(def => {
if ((def.kind === 'ObjectTypeDefinition' || def.kind === 'ObjectTypeExtension') && def.name.value === objectType) {
def?.fields?.forEach(field => {
if (field.name.value === fieldName) {
hasField = true;
}
});
}
});
return hasField;
};

const hasGeneratedFieldArgument = (doc: DocumentNode, objectType: string, fieldName: string, generatedFieldName: string): boolean => {
let hasFieldArgument = false;
doc?.definitions?.forEach(def => {
if ((def.kind === 'ObjectTypeDefinition' || def.kind === 'ObjectTypeExtension') && def.name.value === objectType) {
def?.fields?.forEach(field => {
if (field.name.value === fieldName) {
field?.directives?.forEach(dir => {
if (dir.name.value === 'hasOne') {
dir?.arguments?.forEach(arg => {
if (arg.name.value === 'fields') {
if (arg.value.kind === 'ListValue' && arg.value.values[0].kind === 'StringValue' && arg.value.values[0].value === generatedFieldName) {
hasFieldArgument = true;
}
else if (arg.value.kind === 'StringValue' && arg.value.value === generatedFieldName) {
hasFieldArgument = true;
}
}
});
}
});
}
Comment on lines +488 to +506
Copy link
Contributor

@sundersc sundersc Jun 8, 2022

Choose a reason for hiding this comment

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

nit: Is it possible to reduce the nesting here using filter and some?
May be like doc.definitions.filter(def => (def.kind === '' and so on) && def.fields.some(f => f.name.value === fieldName)).

});
}
});
return hasFieldArgument;
};

beforeEach(() => {
transformer = new GraphQLTransform({
transformers: [new ModelTransformer(), new HasOneTransformer()],
});
});

test('Should generate connecting field when one is not provided', () => {
const schema = `
type Blog @model {
id: ID!
blogName: BlogName @hasOne
}

type BlogName @model {
id: ID!
}
`;

const updatedSchemaDoc = transformer.preProcessSchema(parse(schema));
expect(hasGeneratedField(updatedSchemaDoc, 'Blog', 'blogBlogNameId')).toBeTruthy();
expect(hasGeneratedFieldArgument(updatedSchemaDoc, 'Blog', 'blogName', 'blogBlogNameId')).toBeTruthy();
});

test('Should generate connecting field when empty fields are provided', () => {
const schema = `
type Blog @model {
id: ID!
blogName: BlogName @hasOne(fields: [])
}

type BlogName @model {
id: ID!
}
`;

const updatedSchemaDoc = transformer.preProcessSchema(parse(schema));
expect(hasGeneratedField(updatedSchemaDoc, 'Blog', 'blogBlogNameId')).toBeTruthy();
expect(hasGeneratedFieldArgument(updatedSchemaDoc, 'Blog', 'blogName', 'blogBlogNameId')).toBeTruthy();
});

test('Should not generate connecting field when one is provided', () => {
const schema = `
type Blog @model {
id: ID!
connectionField: ID
blogName: BlogName @hasOne(fields: "connectionField")
}

type BlogName @model {
id: ID!
}
`;

const updatedSchemaDoc = transformer.preProcessSchema(parse(schema));
expect(hasGeneratedField(updatedSchemaDoc, 'Blog', 'blogBlogNameId')).toBeFalsy();
expect(hasGeneratedFieldArgument(updatedSchemaDoc, 'Blog', 'blogName', 'blogBlogNameId')).toBeFalsy();
});

test('Should not generate connecting field when a list one is provided', () => {
const schema = `
type Blog @model {
id: ID!
connectionField: ID
blogName: BlogName @hasOne(fields: ["connectionField"])
}

type BlogName @model {
id: ID!
}
`;

const updatedSchemaDoc = transformer.preProcessSchema(parse(schema));
expect(hasGeneratedField(updatedSchemaDoc, 'Blog', 'blogBlogNameId')).toBeFalsy();
expect(hasGeneratedFieldArgument(updatedSchemaDoc, 'Blog', 'blogName', 'blogBlogNameId')).toBeFalsy();
});
});
Loading