Skip to content

Commit

Permalink
fix(graphql-transformer-common): improve generated graphql pluralization
Browse files Browse the repository at this point in the history
This commit adds the `pluralize` npm library instead of blindly appending an "s" to acheive
pluralization. For example, a model named "Match" should pluralize to "Matches", but before this
commit Amplify would pluralize it to "Matchs"

fix #4224
  • Loading branch information
johnpc committed May 21, 2021
1 parent 3dbb3bf commit 0843e8d
Show file tree
Hide file tree
Showing 36 changed files with 543 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ const path = require('path');
const { RelationalDBSchemaTransformer } = require('graphql-relational-schema-transformer');
const { RelationalDBTemplateGenerator, AuroraServerlessMySQLDatabaseReader } = require('graphql-relational-schema-transformer');
const { mergeTypeDefs } = require('@graphql-tools/merge');
const { ResourceDoesNotExistError, exitOnNextTick } = require('amplify-cli-core');

const { FeatureFlags, ResourceDoesNotExistError, exitOnNextTick } = require('amplify-cli-core');
const subcommand = 'add-graphql-datasource';
const categories = 'categories';
const category = 'api';
Expand Down Expand Up @@ -143,7 +142,11 @@ module.exports = {
context[rdsResourceName] = resourceName;
context[rdsDatasource] = datasource;
let template = templateGenerator.createTemplate(context);
template = templateGenerator.addRelationalResolvers(template, resolversDir);
template = templateGenerator.addRelationalResolvers(
template,
resolversDir,
FeatureFlags.getBoolean('graphqltransformer.improvePluralization'),
);
const cfn = templateGenerator.printCloudformationTemplate(template);

/**
Expand Down
8 changes: 7 additions & 1 deletion packages/amplify-cli-core/src/feature-flags/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,12 @@ export class FeatureFlags {
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'improvePluralization',
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
},
{
name: 'validateTypeNameReservedWords',
type: 'boolean',
Expand Down Expand Up @@ -619,7 +625,7 @@ export class FeatureFlags {
type: 'boolean',
defaultValueForExistingProjects: false,
defaultValueForNewProjects: true,
}
},
]);

this.registerFlag('appSync', [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ import { SearchableModelTransformer } from '../';
import { ModelTransformer } from '@aws-amplify/graphql-model-transformer';
import { anything, countResources, expect as cdkExpect, haveResource } from '@aws-cdk/assert';
import { parse } from 'graphql';
const featureFlags = {
getBoolean: jest.fn().mockImplementation((name, defaultValue) => {
if (name === 'improvePluralization') {
return true;
}
return;
}),
getNumber: jest.fn(),
getObject: jest.fn(),
getString: jest.fn(),
};

test('Test SearchableModelTransformer validation happy case', () => {
const validSchema = `
Expand All @@ -15,6 +26,7 @@ test('Test SearchableModelTransformer validation happy case', () => {
`;
const transformer = new GraphQLTransform({
transformers: [new ModelTransformer(), new SearchableModelTransformer()],
featureFlags,
});
const out = transformer.transform(validSchema);
expect(out).toBeDefined();
Expand All @@ -33,6 +45,7 @@ test('Test SearchableModelTransformer vtl', () => {
`;
const transformer = new GraphQLTransform({
transformers: [new ModelTransformer(), new SearchableModelTransformer()],
featureFlags,
});

const out = transformer.transform(validSchema);
Expand All @@ -50,6 +63,7 @@ test('Test SearchableModelTransformer with query overrides', () => {
`;
const transformer = new GraphQLTransform({
transformers: [new ModelTransformer(), new SearchableModelTransformer()],
featureFlags,
});
const out = transformer.transform(validSchema);
expect(out).toBeDefined();
Expand All @@ -67,6 +81,7 @@ test('Test SearchableModelTransformer with only create mutations', () => {
`;
const transformer = new GraphQLTransform({
transformers: [new ModelTransformer(), new SearchableModelTransformer()],
featureFlags,
});
const out = transformer.transform(validSchema);
expect(out).toBeDefined();
Expand All @@ -90,6 +105,7 @@ test('Test SearchableModelTransformer with multiple model searchable directives'
`;
const transformer = new GraphQLTransform({
transformers: [new ModelTransformer(), new SearchableModelTransformer()],
featureFlags,
});
const out = transformer.transform(validSchema);
expect(out).toBeDefined();
Expand All @@ -108,6 +124,7 @@ test('Test SearchableModelTransformer with sort fields', () => {
`;
const transformer = new GraphQLTransform({
transformers: [new ModelTransformer(), new SearchableModelTransformer()],
featureFlags,
});
const out = transformer.transform(validSchema);
expect(out).toBeDefined();
Expand Down Expand Up @@ -137,6 +154,7 @@ test('it generates expected resources', () => {
`;
const transformer = new GraphQLTransform({
transformers: [new ModelTransformer(), new SearchableModelTransformer()],
featureFlags,
});
const out = transformer.transform(validSchema);
expect(out).toBeDefined();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ export class SearchableModelTransformer extends TransformerPluginBase {
searchFieldNameOverride = directiveArguments.queries.search;
}
}
const fieldName = searchFieldNameOverride ? searchFieldNameOverride : graphqlName(`search${plurality(toUpper(definition.name.value))}`);
const fieldName = searchFieldNameOverride
? searchFieldNameOverride
: graphqlName(`search${plurality(toUpper(definition.name.value), ctx.featureFlags.getBoolean('improvePluralization'))}`);
this.searchableObjectTypeDefinitions.push({
node: definition,
fieldName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export interface GraphQLTransformOptions {
readonly authConfig?: AppSyncAuthConfiguration;
readonly buildParameters?: Record<string, any>;
readonly stacks?: Record<string, Template>;
readonly featuerFlags?: FeatureFlagProvider;
readonly featureFlags?: FeatureFlagProvider;
}
export type StackMapping = { [resourceId: string]: string };
export class GraphQLTransform {
Expand Down Expand Up @@ -110,7 +110,7 @@ export class GraphQLTransform {
this.seenTransformations = {};
const parsedDocument = parse(schema);
this.app = new App();
const context = new TransformerContext(this.app, parsedDocument, this.stackMappingOverrides, this.options.featuerFlags);
const context = new TransformerContext(this.app, parsedDocument, this.stackMappingOverrides, this.options.featureFlags);
const validDirectiveNameMap = this.transformers.reduce(
(acc: any, t: TransformerPluginProvider) => ({ ...acc, [t.directive.name.value]: true }),
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,20 @@ export class TransformerContext implements TransformerContextProvider {
public readonly resourceHelper: TransformerResourceHelper;
public readonly featureFlags: FeatureFlagProvider;
public _api?: GraphQLAPIProvider;
constructor(app: App, public readonly inputDocument: DocumentNode, stackMapping: Record<string, string>, featuerFlags?: FeatureFlagProvider) {
constructor(
app: App,
public readonly inputDocument: DocumentNode,
stackMapping: Record<string, string>,
featureFlags?: FeatureFlagProvider,
) {
this.output = new TransformerOutput(inputDocument);
this.resolvers = new ResolverManager();
this.dataSources = new TransformerDataSourceManager();
this.providerRegistry = new TransformerContextProviderRegistry();
const stackManager = new StackManager(app, stackMapping);
this.stackManager = stackManager
const stackManager = new StackManager(app, stackMapping);
this.stackManager = stackManager;
this.resourceHelper = new TransformerResourceHelper(stackManager);
this.featureFlags = featuerFlags ?? new NoopFeatureFlagProvider();
this.featureFlags = featureFlags ?? new NoopFeatureFlagProvider();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ async function _buildProject(opts: ProjectOptions<TransformerFactoryArgs>) {
authConfig: opts.authConfig,
buildParameters: opts.buildParameters,
stacks: opts.projectConfig.stacks || {},
featuerFlags: new AmplifyCLIFeatureFlagAdapter(),
featureFlags: new AmplifyCLIFeatureFlagAdapter(),
});
return transform.transform(userProjectConfig.schema.toString());
}
8 changes: 6 additions & 2 deletions packages/graphql-auth-transformer/src/ModelAuthTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ export class ModelAuthTransformer extends Transformer {
this.propagateAuthDirectivesToNestedTypes(def, rules, ctx);

// Retrieve the configuration options for the related @model directive
const modelConfiguration = new ModelDirectiveConfiguration(modelDirective, def);
const modelConfiguration = new ModelDirectiveConfiguration(modelDirective, def, ctx.featureFlags.getBoolean('improvePluralization'));

// Get the directives we need to add to the GraphQL nodes
const directives = this.getDirectivesForRules(rules, rules.length === 0 ? this.shouldAddDefaultAuthDirective() : false);
Expand Down Expand Up @@ -516,7 +516,11 @@ Static group authorization should perform as expected.`,
const isDeleteRule = isOpRule('delete');

// Retrieve the configuration options for the related @model directive
const modelConfiguration = new ModelDirectiveConfiguration(modelDirective, parent);
const modelConfiguration = new ModelDirectiveConfiguration(
modelDirective,
parent,
ctx.featureFlags.getBoolean('improvePluralization'),
);
// The field handler adds the read rule on the object
const readRules = rules.filter((rule: AuthRule) => isReadRule(rule));
this.protectReadForField(ctx, parent, definition, readRules, modelConfiguration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ type ModelDirectiveOperation = {
export class ModelDirectiveConfiguration {
map: Map<ModelDirectiveOperationType, ModelDirectiveOperation> = new Map();

constructor(directive: DirectiveNode, def: ObjectTypeDefinitionNode) {
constructor(directive: DirectiveNode, def: ObjectTypeDefinitionNode, improvePluralization: boolean) {
const typeName = def.name.value;
const directiveArguments: ModelDirectiveArgs = getDirectiveArguments(directive);

const makeName = (operation: ModelDirectiveOperationType, nameOverride?: string, isList: boolean = false) =>
nameOverride ? nameOverride : graphqlName(operation + (isList ? plurality(toUpper(typeName)) : toUpper(typeName)));
nameOverride
? nameOverride
: graphqlName(operation + (isList ? plurality(toUpper(typeName), improvePluralization) : toUpper(typeName)));

let shouldHaveCreate = true;
let shouldHaveUpdate = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { GraphQLTransform } from 'graphql-transformer-core';
import { ResourceConstants } from 'graphql-transformer-common';
import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer';
import { ModelAuthTransformer } from '../ModelAuthTransformer';
import _ from 'lodash';
const featureFlags = {
getBoolean: jest.fn().mockImplementation((name, defaultValue) => {
if (name === 'improvePluralization') {
return true;
}
return;
}),
getNumber: jest.fn(),
getObject: jest.fn(),
getString: jest.fn(),
};

test('Test simple model with public auth rule and amplify admin app is present', () => {
const validSchema = `
Expand All @@ -14,6 +24,7 @@ test('Test simple model with public auth rule and amplify admin app is present',
}
`;
const transformer = new GraphQLTransform({
featureFlags,
transformers: [
new DynamoDBModelTransformer(),
new ModelAuthTransformer({
Expand Down Expand Up @@ -46,6 +57,7 @@ test('Test simple model with public auth rule and amplify admin app is not enabl
}
`;
const transformer = new GraphQLTransform({
featureFlags,
transformers: [
new DynamoDBModelTransformer(),
new ModelAuthTransformer({
Expand Down Expand Up @@ -74,6 +86,7 @@ test('Test simple model with private auth rule and amplify admin app is present'
}
`;
const transformer = new GraphQLTransform({
featureFlags,
transformers: [
new DynamoDBModelTransformer(),
new ModelAuthTransformer({
Expand Down Expand Up @@ -106,6 +119,7 @@ test('Test simple model with private auth rule and amplify admin app not enabled
}
`;
const transformer = new GraphQLTransform({
featureFlags,
transformers: [
new DynamoDBModelTransformer(),
new ModelAuthTransformer({
Expand Down Expand Up @@ -138,6 +152,7 @@ test('Test model with public auth rule without all operations and amplify admin
}
`;
const transformer = new GraphQLTransform({
featureFlags,
transformers: [
new DynamoDBModelTransformer(),
new ModelAuthTransformer({
Expand Down Expand Up @@ -182,6 +197,7 @@ test('Test simple model with private auth rule, few operations, and amplify admi
}
`;
const transformer = new GraphQLTransform({
featureFlags,
transformers: [
new DynamoDBModelTransformer(),
new ModelAuthTransformer({
Expand Down Expand Up @@ -225,6 +241,7 @@ test('Test simple model with private IAM auth rule, few operations, and amplify
}
`;
const transformer = new GraphQLTransform({
featureFlags,
transformers: [
new DynamoDBModelTransformer(),
new ModelAuthTransformer({
Expand Down Expand Up @@ -263,6 +280,7 @@ test('Test simple model with AdminUI enabled should add IAM policy only for fiel
}
`;
const transformer = new GraphQLTransform({
featureFlags,
transformers: [
new DynamoDBModelTransformer(),
new ModelAuthTransformer({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ import { GraphQLTransform } from 'graphql-transformer-core';
import { ResourceConstants } from 'graphql-transformer-common';
import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer';
import { ModelAuthTransformer } from '../ModelAuthTransformer';
const featureFlags = {
getBoolean: jest.fn().mockImplementation((name, defaultValue) => {
if (name === 'improvePluralization') {
return true;
}
return;
}),
getNumber: jest.fn(),
getObject: jest.fn(),
getString: jest.fn(),
};

test('Test ModelAuthTransformer validation happy case w/ static groups', () => {
const validSchema = `
Expand All @@ -13,6 +24,7 @@ test('Test ModelAuthTransformer validation happy case w/ static groups', () => {
}
`;
const transformer = new GraphQLTransform({
featureFlags,
transformers: [
new DynamoDBModelTransformer(),
new ModelAuthTransformer({
Expand All @@ -28,7 +40,7 @@ test('Test ModelAuthTransformer validation happy case w/ static groups', () => {
const out = transformer.transform(validSchema);
expect(out).toBeDefined();
expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual(
'AMAZON_COGNITO_USER_POOLS'
'AMAZON_COGNITO_USER_POOLS',
);
});

Expand All @@ -43,6 +55,7 @@ test('Test ModelAuthTransformer validation happy case w/ dynamic groups', () =>
}
`;
const transformer = new GraphQLTransform({
featureFlags,
transformers: [
new DynamoDBModelTransformer(),
new ModelAuthTransformer({
Expand All @@ -58,7 +71,7 @@ test('Test ModelAuthTransformer validation happy case w/ dynamic groups', () =>
const out = transformer.transform(validSchema);
expect(out).toBeDefined();
expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual(
'AMAZON_COGNITO_USER_POOLS'
'AMAZON_COGNITO_USER_POOLS',
);
});

Expand All @@ -73,6 +86,7 @@ test('Test ModelAuthTransformer validation happy case w/ dynamic group', () => {
}
`;
const transformer = new GraphQLTransform({
featureFlags,
transformers: [
new DynamoDBModelTransformer(),
new ModelAuthTransformer({
Expand All @@ -88,7 +102,7 @@ test('Test ModelAuthTransformer validation happy case w/ dynamic group', () => {
const out = transformer.transform(validSchema);
expect(out).toBeDefined();
expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual(
'AMAZON_COGNITO_USER_POOLS'
'AMAZON_COGNITO_USER_POOLS',
);
});

Expand All @@ -104,6 +118,7 @@ test('Test ModelAuthTransformer validation @auth on non @model. Should fail.', (
}
`;
const transformer = new GraphQLTransform({
featureFlags,
transformers: [
new DynamoDBModelTransformer(),
new ModelAuthTransformer({
Expand Down
Loading

0 comments on commit 0843e8d

Please sign in to comment.