From f5d662b65ba160542cdcead36ee15967920afd5b Mon Sep 17 00:00:00 2001 From: John Corser Date: Mon, 21 Jun 2021 18:15:45 -0400 Subject: [PATCH] fix(graphql-transformer-common): improve generated graphql pluralization (#7258) 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 --- .../commands/api/add-graphql-datasource.js | 9 +- .../src/feature-flags/featureFlags.ts | 8 +- .../tests/data-access-patterns.ts | 8 +- ...fy-graphql-searchable-transformer.tests.ts | 18 +++ .../src/graphql-searchable-transformer.ts | 4 +- .../src/transformation/transform.ts | 4 +- .../src/transformer-context/index.ts | 13 +- .../transform-graphql-schema.ts | 2 +- .../model-auth-transformer.e2e.test.ts | 20 +-- .../src/ModelAuthTransformer.ts | 8 +- .../src/ModelDirectiveConfiguration.ts | 6 +- .../src/__tests__/AmplifyAdminAuth.test.ts | 20 ++- .../__tests__/GroupAuthTransformer.test.ts | 21 +++- .../src/__tests__/MultiAuth.test.ts | 18 ++- .../__tests__/NonModelAuthTransformer.test.ts | 19 +++ .../src/__tests__/OperationsArgument.test.ts | 17 +++ .../__tests__/OwnerAuthTransformer.test.ts | 18 ++- .../__tests__/PerFieldAuthArgument.test.ts | 19 ++- .../SearchableAuthTransformer.test.ts | 17 ++- .../ModelConnectionTransformer.test.ts | 35 +++++- .../NewConnectionTransformer.test.ts | 32 +++++ .../src/DynamoDBModelTransformer.ts | 8 +- .../DynamoDBModelTransformer.test.ts | 39 ++++++ .../DynamoDBModelTransformer.test.ts.snap | 2 +- .../src/resources.ts | 10 +- .../src/SearchableModelTransformer.ts | 1 + .../SearchableModelTransformer.test.ts | 17 +++ .../src/resources.ts | 3 +- .../src/__tests__/KeyTransformer.test.ts | 52 +++++--- .../src/RelationalDBResolverGenerator.ts | 8 +- .../src/RelationalDBTemplateGenerator.ts | 4 +- .../RelationalDBResolverGenerator.test.ts | 45 +++++-- .../RelationalDBTemplateGenerator.test.ts | 2 +- ...RelationalDBResolverGenerator.test.ts.snap | 116 +++++++++++++++++- .../graphql-transformer-common/package.json | 3 +- .../graphql-transformer-common/src/util.ts | 9 +- .../ConnectionsWithAuthTests.e2e.test.ts | 12 ++ .../src/__tests__/CustomRoots.e2e.test.ts | 14 +++ .../DynamoDBModelTransformer.e2e.test.ts | 13 +- .../FunctionTransformerTests.e2e.test.ts | 13 +- .../src/__tests__/HttpTransformer.e2e.test.ts | 13 +- .../src/__tests__/KeyTransformer.e2e.test.ts | 13 +- .../__tests__/KeyTransformerLocal.e2e.test.ts | 61 ++++++--- .../src/__tests__/KeyWithAuth.e2e.test.ts | 13 +- .../ModelAuthTransformer.e2e.test.ts | 33 +++-- .../ModelConnectionTransformer.e2e.test.ts | 13 +- ...elConnectionWithKeyTransformer.e2e.test.ts | 13 +- .../MultiAuthModelAuthTransformer.e2e.test.ts | 13 +- .../__tests__/MutationCondition.e2e.test.ts | 14 ++- .../__tests__/NestedStacksTest.e2e.test.ts | 13 +- .../NewConnectionTransformer.e2e.test.ts | 13 +- .../NewConnectionWithAuth.e2e.test.ts | 13 +- .../NonModelAuthFunction.e2e.test.ts | 13 +- .../NoneEnvFunctionTransformer.e2e.test.ts | 13 +- .../__tests__/PerFieldAuthTests.e2e.test.ts | 13 +- .../PredictionsTransformerTests.e2e.test.ts | 13 +- .../SearchableModelTransformer.e2e.test.ts | 12 ++ .../SearchableWithAuthTests.e2e.test.ts | 13 +- .../SubscriptionsWithAuthTest.e2e.test.ts | 13 +- .../TestComplexStackMappingsLocal.e2e.test.ts | 28 +++-- .../VersionedModelTransformer.e2e.test.ts | 13 +- .../VersionedModelTransformer.test.ts | 15 ++- yarn.lock | 2 +- 63 files changed, 908 insertions(+), 152 deletions(-) diff --git a/packages/amplify-category-api/src/commands/api/add-graphql-datasource.js b/packages/amplify-category-api/src/commands/api/add-graphql-datasource.js index 957fdce03e9..415da07562f 100644 --- a/packages/amplify-category-api/src/commands/api/add-graphql-datasource.js +++ b/packages/amplify-category-api/src/commands/api/add-graphql-datasource.js @@ -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'; @@ -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); /** diff --git a/packages/amplify-cli-core/src/feature-flags/featureFlags.ts b/packages/amplify-cli-core/src/feature-flags/featureFlags.ts index 2e930666235..47abc0c880f 100644 --- a/packages/amplify-cli-core/src/feature-flags/featureFlags.ts +++ b/packages/amplify-cli-core/src/feature-flags/featureFlags.ts @@ -515,6 +515,12 @@ export class FeatureFlags { defaultValueForExistingProjects: false, defaultValueForNewProjects: true, }, + { + name: 'improvePluralization', + type: 'boolean', + defaultValueForExistingProjects: false, + defaultValueForNewProjects: true, + }, { name: 'validateTypeNameReservedWords', type: 'boolean', @@ -619,7 +625,7 @@ export class FeatureFlags { type: 'boolean', defaultValueForExistingProjects: false, defaultValueForNewProjects: true, - } + }, ]); this.registerFlag('appSync', [ diff --git a/packages/amplify-e2e-tests/src/schema-api-directives/tests/data-access-patterns.ts b/packages/amplify-e2e-tests/src/schema-api-directives/tests/data-access-patterns.ts index 2dd1fb815e0..5c96232ee62 100644 --- a/packages/amplify-e2e-tests/src/schema-api-directives/tests/data-access-patterns.ts +++ b/packages/amplify-e2e-tests/src/schema-api-directives/tests/data-access-patterns.ts @@ -368,7 +368,7 @@ export const expected_result_query6 = { export const query7 = ` ## 7. See all employees hired recently: -#Having '@key(name: "newHire", fields: ["newHire", "id"])' on the 'Employee' model allows one to query by whether an employee has been hired recently. +#Having '@key(name: "newHire", fields: ["newHire", "id"])' on the 'Employee' model allows one to query by whether an employee has been hired recently. query employeesNewHire { employeesNewHire(newHire: "true") { @@ -685,8 +685,8 @@ export const query16 = ` ## 16. Get total product inventory: #How this would be done depends on the use case. If one just wants a list of all inventories in all warehouses, one could just run a list inventories on the Inventory model: -query listInventorys { - listInventorys { +query listInventories { + listInventories { items { productID warehouseID @@ -696,7 +696,7 @@ query listInventorys { }`; export const expected_result_query16 = { data: { - listInventorys: { + listInventories: { items: [ { productID: 'yeezyboost', diff --git a/packages/amplify-graphql-searchable-transformer/src/__tests__/amplify-graphql-searchable-transformer.tests.ts b/packages/amplify-graphql-searchable-transformer/src/__tests__/amplify-graphql-searchable-transformer.tests.ts index 613c4c0ab04..45be2e58cf8 100644 --- a/packages/amplify-graphql-searchable-transformer/src/__tests__/amplify-graphql-searchable-transformer.tests.ts +++ b/packages/amplify-graphql-searchable-transformer/src/__tests__/amplify-graphql-searchable-transformer.tests.ts @@ -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 = ` @@ -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(); @@ -33,6 +45,7 @@ test('Test SearchableModelTransformer vtl', () => { `; const transformer = new GraphQLTransform({ transformers: [new ModelTransformer(), new SearchableModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); diff --git a/packages/amplify-graphql-searchable-transformer/src/graphql-searchable-transformer.ts b/packages/amplify-graphql-searchable-transformer/src/graphql-searchable-transformer.ts index db39d2f9cf3..ef36cd4827f 100644 --- a/packages/amplify-graphql-searchable-transformer/src/graphql-searchable-transformer.ts +++ b/packages/amplify-graphql-searchable-transformer/src/graphql-searchable-transformer.ts @@ -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, diff --git a/packages/amplify-graphql-transformer-core/src/transformation/transform.ts b/packages/amplify-graphql-transformer-core/src/transformation/transform.ts index 0bd632467b9..3442ce2d12c 100644 --- a/packages/amplify-graphql-transformer-core/src/transformation/transform.ts +++ b/packages/amplify-graphql-transformer-core/src/transformation/transform.ts @@ -61,7 +61,7 @@ export interface GraphQLTransformOptions { readonly authConfig?: AppSyncAuthConfiguration; readonly buildParameters?: Record; readonly stacks?: Record; - readonly featuerFlags?: FeatureFlagProvider; + readonly featureFlags?: FeatureFlagProvider; } export type StackMapping = { [resourceId: string]: string }; export class GraphQLTransform { @@ -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 }), { diff --git a/packages/amplify-graphql-transformer-core/src/transformer-context/index.ts b/packages/amplify-graphql-transformer-core/src/transformer-context/index.ts index 5a3a2ef5959..002893fa58c 100644 --- a/packages/amplify-graphql-transformer-core/src/transformer-context/index.ts +++ b/packages/amplify-graphql-transformer-core/src/transformer-context/index.ts @@ -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, featuerFlags?: FeatureFlagProvider) { + constructor( + app: App, + public readonly inputDocument: DocumentNode, + stackMapping: Record, + 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(); } /** diff --git a/packages/amplify-provider-awscloudformation/src/graphql-transformer/transform-graphql-schema.ts b/packages/amplify-provider-awscloudformation/src/graphql-transformer/transform-graphql-schema.ts index e9c01823397..8b27d65556b 100644 --- a/packages/amplify-provider-awscloudformation/src/graphql-transformer/transform-graphql-schema.ts +++ b/packages/amplify-provider-awscloudformation/src/graphql-transformer/transform-graphql-schema.ts @@ -414,7 +414,7 @@ async function _buildProject(opts: ProjectOptions) { authConfig: opts.authConfig, buildParameters: opts.buildParameters, stacks: opts.projectConfig.stacks || {}, - featuerFlags: new AmplifyCLIFeatureFlagAdapter(), + featureFlags: new AmplifyCLIFeatureFlagAdapter(), }); return transform.transform(userProjectConfig.schema.toString()); } diff --git a/packages/amplify-util-mock/src/__e2e__/model-auth-transformer.e2e.test.ts b/packages/amplify-util-mock/src/__e2e__/model-auth-transformer.e2e.test.ts index bb2a1c68e19..8391b5ea2e1 100644 --- a/packages/amplify-util-mock/src/__e2e__/model-auth-transformer.e2e.test.ts +++ b/packages/amplify-util-mock/src/__e2e__/model-auth-transformer.e2e.test.ts @@ -792,7 +792,7 @@ test(`Test getSalary w/ Admin group protection not authorized`, async () => { expect((req2.errors[0] as any).errorType).toEqual('Unauthorized'); }); -test(`Test listSalarys w/ Admin group protection authorized`, async () => { +test(`Test listSalaries w/ Admin group protection authorized`, async () => { const req = await GRAPHQL_CLIENT_1.query(` mutation { createSalary(input: { wage: 101 }) { @@ -806,7 +806,7 @@ test(`Test listSalarys w/ Admin group protection authorized`, async () => { expect(req.data.createSalary.wage).toEqual(101); const req2 = await GRAPHQL_CLIENT_1.query(` query { - listSalarys(filter: { wage: { eq: 101 }}) { + listSalaries(filter: { wage: { eq: 101 }}) { items { id wage @@ -814,12 +814,12 @@ test(`Test listSalarys w/ Admin group protection authorized`, async () => { } } `); - expect(req2.data.listSalarys.items.length).toEqual(1); - expect(req2.data.listSalarys.items[0].id).toEqual(req.data.createSalary.id); - expect(req2.data.listSalarys.items[0].wage).toEqual(101); + expect(req2.data.listSalaries.items.length).toEqual(1); + expect(req2.data.listSalaries.items[0].id).toEqual(req.data.createSalary.id); + expect(req2.data.listSalaries.items[0].wage).toEqual(101); }); -test(`Test listSalarys w/ Admin group protection not authorized`, async () => { +test(`Test listSalaries w/ Admin group protection not authorized`, async () => { const req = await GRAPHQL_CLIENT_1.query(` mutation { createSalary(input: { wage: 102 }) { @@ -833,7 +833,7 @@ test(`Test listSalarys w/ Admin group protection not authorized`, async () => { expect(req.data.createSalary.wage).toEqual(102); const req2 = await GRAPHQL_CLIENT_2.query(` query { - listSalarys(filter: { wage: { eq: 102 }}) { + listSalaries(filter: { wage: { eq: 102 }}) { items { id wage @@ -841,7 +841,7 @@ test(`Test listSalarys w/ Admin group protection not authorized`, async () => { } } `); - expect(req2.data.listSalarys.items).toEqual([]); + expect(req2.data.listSalaries.items).toEqual([]); }); /** @@ -2374,7 +2374,7 @@ test(`Test createTestIdentity as admin.`, async () => { const listResponse = await GRAPHQL_CLIENT_3.query( `query { - listTestIdentitys(filter: { title: { eq: "Test title update" } }, limit: 100) { + listTestIdentities(filter: { title: { eq: "Test title update" } }, limit: 100) { items { id title @@ -2384,7 +2384,7 @@ test(`Test createTestIdentity as admin.`, async () => { }`, {}, ); - const relevantPost = listResponse.data.listTestIdentitys.items.find(p => p.id === getReq.data.getTestIdentity.id); + const relevantPost = listResponse.data.listTestIdentities.items.find(p => p.id === getReq.data.getTestIdentity.id); logDebug(JSON.stringify(listResponse, null, 4)); expect(relevantPost).toBeTruthy(); expect(relevantPost.title).toEqual('Test title update'); diff --git a/packages/graphql-auth-transformer/src/ModelAuthTransformer.ts b/packages/graphql-auth-transformer/src/ModelAuthTransformer.ts index 7ac88f2106d..920b858e08a 100644 --- a/packages/graphql-auth-transformer/src/ModelAuthTransformer.ts +++ b/packages/graphql-auth-transformer/src/ModelAuthTransformer.ts @@ -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); @@ -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); diff --git a/packages/graphql-auth-transformer/src/ModelDirectiveConfiguration.ts b/packages/graphql-auth-transformer/src/ModelDirectiveConfiguration.ts index 744718a7f52..44b11846f52 100644 --- a/packages/graphql-auth-transformer/src/ModelDirectiveConfiguration.ts +++ b/packages/graphql-auth-transformer/src/ModelDirectiveConfiguration.ts @@ -41,12 +41,14 @@ type ModelDirectiveOperation = { export class ModelDirectiveConfiguration { map: Map = 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; diff --git a/packages/graphql-auth-transformer/src/__tests__/AmplifyAdminAuth.test.ts b/packages/graphql-auth-transformer/src/__tests__/AmplifyAdminAuth.test.ts index e4ab14130b1..a5e3bf380d7 100644 --- a/packages/graphql-auth-transformer/src/__tests__/AmplifyAdminAuth.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/AmplifyAdminAuth.test.ts @@ -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 = ` @@ -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({ @@ -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({ @@ -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({ @@ -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({ @@ -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({ @@ -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({ @@ -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({ @@ -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({ diff --git a/packages/graphql-auth-transformer/src/__tests__/GroupAuthTransformer.test.ts b/packages/graphql-auth-transformer/src/__tests__/GroupAuthTransformer.test.ts index a1cd98a8a01..d184c517fd3 100644 --- a/packages/graphql-auth-transformer/src/__tests__/GroupAuthTransformer.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/GroupAuthTransformer.test.ts @@ -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 = ` @@ -13,6 +24,7 @@ test('Test ModelAuthTransformer validation happy case w/ static groups', () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -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', ); }); @@ -43,6 +55,7 @@ test('Test ModelAuthTransformer validation happy case w/ dynamic groups', () => } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -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', ); }); @@ -73,6 +86,7 @@ test('Test ModelAuthTransformer validation happy case w/ dynamic group', () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -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', ); }); @@ -104,6 +118,7 @@ test('Test ModelAuthTransformer validation @auth on non @model. Should fail.', ( } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ diff --git a/packages/graphql-auth-transformer/src/__tests__/MultiAuth.test.ts b/packages/graphql-auth-transformer/src/__tests__/MultiAuth.test.ts index 4287ce66c12..63e097c1f52 100644 --- a/packages/graphql-auth-transformer/src/__tests__/MultiAuth.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/MultiAuth.test.ts @@ -4,6 +4,17 @@ import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; import { ModelConnectionTransformer } from 'graphql-connection-transformer'; import { ModelAuthTransformer, AppSyncAuthConfiguration, AppSyncAuthMode } from '../ModelAuthTransformer'; import { KeyTransformer } from '../../../graphql-key-transformer/src/KeyTransformer'; +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const noAuthModeDefaultConfig: AppSyncAuthConfiguration = { defaultAuthentication: { @@ -180,6 +191,7 @@ const getRecursiveSchemaWithDiffModesOnParentType = (authDir1: string, authDir2: const getTransformer = (authConfig: AppSyncAuthConfiguration) => new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new KeyTransformer(), @@ -730,7 +742,7 @@ describe('Type directive transformation tests', () => { @auth(rules: [ { allow: private, provider: iam, operations: [read] }, { allow: groups, groups: ["Group"], operations: [read, update, delete] } - ]) + ]) @key(name: "byUser", fields: ["postUserId"]) { id: ID! @@ -768,7 +780,7 @@ describe('Type directive transformation tests', () => { title: String! editors: [PostEditor] @connection(keyName: "byPost", fields: ["id"]) } - + # Create a join model and disable queries as you don't need them # and can query through Post.editors and User.posts type PostEditor @@ -782,7 +794,7 @@ describe('Type directive transformation tests', () => { post: Post! @connection(fields: ["postID"]) editor: User! @connection(fields: ["editorID"]) } - + type User @model @auth(rules: [{ allow: owner }]) { id: ID! username: String! diff --git a/packages/graphql-auth-transformer/src/__tests__/NonModelAuthTransformer.test.ts b/packages/graphql-auth-transformer/src/__tests__/NonModelAuthTransformer.test.ts index 8b523ebc4a4..f3702bb5dfc 100644 --- a/packages/graphql-auth-transformer/src/__tests__/NonModelAuthTransformer.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/NonModelAuthTransformer.test.ts @@ -3,6 +3,17 @@ import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; import { FunctionTransformer } from 'graphql-function-transformer'; import { ModelAuthTransformer } from '../ModelAuthTransformer'; import { ObjectTypeDefinitionNode, DocumentNode, Kind, 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(), +}; describe('@auth directive without @model', async () => { const getObjectType = (doc: DocumentNode, type: string): ObjectTypeDefinitionNode | undefined => { @@ -36,6 +47,7 @@ describe('@auth directive without @model', async () => { `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -70,6 +82,7 @@ describe('@auth directive without @model', async () => { `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -104,6 +117,7 @@ operations will be generated by the CLI.`); `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -137,6 +151,7 @@ are already on an operation already.`); `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -174,6 +189,7 @@ are already on an operation already.`); `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -212,6 +228,7 @@ are already on an operation already.`); `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -251,6 +268,7 @@ are already on an operation already.`); `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new FunctionTransformer(), @@ -289,6 +307,7 @@ are already on an operation already.`); `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new FunctionTransformer(), diff --git a/packages/graphql-auth-transformer/src/__tests__/OperationsArgument.test.ts b/packages/graphql-auth-transformer/src/__tests__/OperationsArgument.test.ts index a50edbac8ee..d0fbf598632 100644 --- a/packages/graphql-auth-transformer/src/__tests__/OperationsArgument.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/OperationsArgument.test.ts @@ -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 "read" auth operation', () => { const validSchema = ` @@ -13,6 +24,7 @@ test('Test "read" auth operation', () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -44,6 +56,7 @@ test('Test "create", "update", "delete" auth operations', () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -83,6 +96,7 @@ test('Test that operation overwrites queries in auth operations', () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -124,6 +138,7 @@ test('Test that checks subscription resolvers are generated with auth logic', () } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -164,6 +179,7 @@ test('Test that checks subscription resolvers are created without auth logic', ( } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -207,6 +223,7 @@ test('Test that subscriptions are only generated if the respective mutation oper } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ diff --git a/packages/graphql-auth-transformer/src/__tests__/OwnerAuthTransformer.test.ts b/packages/graphql-auth-transformer/src/__tests__/OwnerAuthTransformer.test.ts index 62caf321a08..a0314833991 100644 --- a/packages/graphql-auth-transformer/src/__tests__/OwnerAuthTransformer.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/OwnerAuthTransformer.test.ts @@ -1,9 +1,20 @@ -import { parse, DefinitionNode, InputObjectTypeDefinitionNode } from 'graphql'; +import { parse } from 'graphql'; import { FeatureFlagProvider, GraphQLTransform } from 'graphql-transformer-core'; import { ResourceConstants } from 'graphql-transformer-common'; import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; import { ModelAuthTransformer } from '../ModelAuthTransformer'; import { getObjectType, getField } from './test-helpers'; +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', () => { const validSchema = ` @@ -15,6 +26,7 @@ test('Test ModelAuthTransformer validation happy case', () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -46,6 +58,7 @@ test('Test OwnerField with Subscriptions', () => { postOwner: String }`; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -102,6 +115,9 @@ describe('add missing implicit owner fields to type', () => { if (name === 'addMissingOwnerFields') { return true; } + if (name === 'improvePluralization') { + return true; + } }), getNumber: jest.fn(), getObject: jest.fn(), diff --git a/packages/graphql-auth-transformer/src/__tests__/PerFieldAuthArgument.test.ts b/packages/graphql-auth-transformer/src/__tests__/PerFieldAuthArgument.test.ts index 6a5237b6df5..aae4910f688 100644 --- a/packages/graphql-auth-transformer/src/__tests__/PerFieldAuthArgument.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/PerFieldAuthArgument.test.ts @@ -4,7 +4,17 @@ import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; import { ModelConnectionTransformer } from 'graphql-connection-transformer'; import { KeyTransformer } from 'graphql-key-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 that subscriptions are only generated if the respective mutation operation exists', () => { const validSchema = ` type Salary @@ -19,6 +29,7 @@ test('Test that subscriptions are only generated if the respective mutation oper secret: String @auth(rules: [{allow: owner}]) }`; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ @@ -61,14 +72,14 @@ test('Test per-field @auth on a @connection field', () => { { id: ID! name: String! - tags: [Tag] + tags: [Tag] @connection(keyName: "byTags", fields: ["id"]) @auth(rules: [ { allow: groups, groups: ["admin"] } ]) } type Tag @model @key(name: "byTags", fields: ["postID"]) - @auth(rules: [ { allow: groups, groups: ["admin"] } ]) + @auth(rules: [ { allow: groups, groups: ["admin"] } ]) { id: ID! postID: ID! @@ -78,6 +89,7 @@ test('Test per-field @auth on a @connection field', () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new KeyTransformer(), @@ -113,6 +125,7 @@ test('Test per-field @auth without model', () => { `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ diff --git a/packages/graphql-auth-transformer/src/__tests__/SearchableAuthTransformer.test.ts b/packages/graphql-auth-transformer/src/__tests__/SearchableAuthTransformer.test.ts index 1d911ad1240..20c3ffdf08d 100644 --- a/packages/graphql-auth-transformer/src/__tests__/SearchableAuthTransformer.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/SearchableAuthTransformer.test.ts @@ -2,6 +2,17 @@ import { GraphQLTransform } from 'graphql-transformer-core'; import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; import { ModelAuthTransformer } from '../ModelAuthTransformer'; import { SearchableModelTransformer } from 'graphql-elasticsearch-transformer'; +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 auth logic is enabled on owner/static rules in resposne es resolver', () => { const validSchema = ` @@ -17,6 +28,7 @@ test('test auth logic is enabled on owner/static rules in resposne es resolver', } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new SearchableModelTransformer(), @@ -35,11 +47,11 @@ test('test auth logic is enabled on owner/static rules in resposne es resolver', // expect response resolver to contain auth logic for owner rule expect(out).toBeDefined(); expect(out.resolvers['Query.searchComments.res.vtl']).toContain( - '## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } **' + '## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } **', ); // expect response resolver to contain auth logic for group rule expect(out.resolvers['Query.searchComments.res.vtl']).toContain( - '## Authorization rule: { allow: groups, groups: ["writer"], groupClaim: "cognito:groups" } **' + '## Authorization rule: { allow: groups, groups: ["writer"], groupClaim: "cognito:groups" } **', ); }); @@ -57,6 +69,7 @@ test('test auth logic is enabled for iam/apiKey auth rules in response es resolv } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new SearchableModelTransformer(), diff --git a/packages/graphql-connection-transformer/src/__tests__/ModelConnectionTransformer.test.ts b/packages/graphql-connection-transformer/src/__tests__/ModelConnectionTransformer.test.ts index 911508be9f9..c4c7bef91bb 100644 --- a/packages/graphql-connection-transformer/src/__tests__/ModelConnectionTransformer.test.ts +++ b/packages/graphql-connection-transformer/src/__tests__/ModelConnectionTransformer.test.ts @@ -12,6 +12,17 @@ import { GraphQLTransform, TRANSFORM_CURRENT_VERSION } from 'graphql-transformer import { ResolverResourceIDs, ModelResourceIDs, ResourceConstants } from 'graphql-transformer-common'; import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; import { ModelConnectionTransformer } from '../ModelConnectionTransformer'; +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 ModelConnectionTransformer simple one to many happy case', () => { const validSchema = ` @@ -27,6 +38,7 @@ test('Test ModelConnectionTransformer simple one to many happy case', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -67,6 +79,7 @@ test('Test ModelConnectionTransformer simple one to many happy case with custom `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -107,6 +120,7 @@ test('Test that ModelConnection Transformer throws error when the field in conne `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); try { transformer.transform(invalidSchema); @@ -135,6 +149,7 @@ test('Test ModelConnectionTransformer simple one to many happy case with custom `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -178,6 +193,7 @@ test('Test ModelConnectionTransformer complex one to many happy case', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -226,6 +242,7 @@ test('Test ModelConnectionTransformer many to many should fail', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); try { transformer.transform(validSchema); @@ -254,6 +271,7 @@ test('Test ModelConnectionTransformer many to many should fail due to missing ot `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); try { transformer.transform(validSchema); @@ -278,6 +296,7 @@ test('Test ModelConnectionTransformer many to many should fail due to missing ot `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -320,6 +339,7 @@ test('Test ModelConnectionTransformer with non null @connections', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -373,6 +393,7 @@ test('Test ModelConnectionTransformer with sortField fails if not specified in a `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => { transformer.transform(validSchema); @@ -395,6 +416,7 @@ test('Test ModelConnectionTransformer with sortField creates a connection resolv `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -412,6 +434,7 @@ test('Test ModelConnectionTransformer with sortField creates a connection resolv test('Test ModelConnectionTransformer throws with invalid key fields', () => { const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const invalidSchema = ` @@ -469,6 +492,7 @@ test('Test ModelConnectionTransformer throws with invalid key fields', () => { test('Test ModelConnectionTransformer does not throw with valid key fields', () => { const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const validSchema = ` @@ -541,6 +565,7 @@ test('Test ModelConnectionTransformer sortField with missing @key should fail', const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); try { @@ -567,6 +592,7 @@ test('Test ModelConnectionTransformer overrides the default limit', () => { const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); @@ -591,6 +617,7 @@ test('Test ModelConnectionTransformer uses the default limit', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -617,6 +644,7 @@ test('Test ModelConnectionTransformer with keyField overrides the default limit' const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); @@ -641,6 +669,7 @@ test('Test ModelConnectionTransformer with keyField uses the default limit', () `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -667,6 +696,7 @@ test('Connection on models with no codegen includes AttributeTypeEnum', () => { const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, transformConfig: { Version: TRANSFORM_CURRENT_VERSION, }, @@ -683,13 +713,13 @@ test('Connection on models with no codegen includes custom enum filters', () => id: ID!, cartItems: [CartItem] @connection(name: "CartCartItem") } - + type CartItem @model(queries: null, mutations: null, subscriptions: null) { id: ID! productType: PRODUCT_TYPE! cart: Cart @connection(name: "CartCartItem") } - + enum PRODUCT_TYPE { UNIT PACKAGE @@ -698,6 +728,7 @@ test('Connection on models with no codegen includes custom enum filters', () => const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, transformConfig: { Version: TRANSFORM_CURRENT_VERSION, }, diff --git a/packages/graphql-connection-transformer/src/__tests__/NewConnectionTransformer.test.ts b/packages/graphql-connection-transformer/src/__tests__/NewConnectionTransformer.test.ts index e5454fb12a4..bb2c42b3467 100644 --- a/packages/graphql-connection-transformer/src/__tests__/NewConnectionTransformer.test.ts +++ b/packages/graphql-connection-transformer/src/__tests__/NewConnectionTransformer.test.ts @@ -13,6 +13,17 @@ import { ResolverResourceIDs } from 'graphql-transformer-common'; import { ModelConnectionTransformer } from '../ModelConnectionTransformer'; import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; import { KeyTransformer } from 'graphql-key-transformer'; +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; test('ModelConnectionTransformer should fail if connection was called on an object that is not a Model type.', () => { const validSchema = ` @@ -30,6 +41,7 @@ test('ModelConnectionTransformer should fail if connection was called on an obje const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => transformer.transform(validSchema)).toThrowError(`@connection must be on an @model object type field.`); @@ -51,6 +63,7 @@ test('ModelConnectionTransformer should fail if connection was with an object th const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => transformer.transform(validSchema)).toThrowError(`Object type Test1 must be annotated with @model.`); @@ -72,6 +85,7 @@ test('ModelConnectionTransformer should fail if the field type where the directi const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => transformer.transform(validSchema)).toThrowError('Unknown type "Test2". Did you mean "Test" or "Test1"?'); @@ -93,6 +107,7 @@ test('ModelConnectionTransformer should fail if an empty list of fields is passe const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => transformer.transform(validSchema)).toThrowError('No fields passed in to @connection directive.'); @@ -118,6 +133,7 @@ test('ModelConnectionTransformer should fail if any of the fields passed in are const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => transformer.transform(validSchema)).toThrowError('name is not a field in Test'); @@ -143,6 +159,7 @@ test('ModelConnectionTransformer should fail if the query is not run on the defa const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => transformer.transform(validSchema)).toThrowError( @@ -167,6 +184,7 @@ test('ModelConnectionTransformer should fail if keyName provided does not exist. const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => transformer.transform(validSchema)).toThrowError('Key notDefault does not exist for model Test1'); @@ -189,6 +207,7 @@ test('ModelConnectionTransformer should fail if first field does not match PK of const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => transformer.transform(validSchema)).toThrowError('email field is not of type ID'); @@ -214,6 +233,7 @@ test('ModelConnectionTransformer should fail if sort key type passed in does not const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => transformer.transform(validSchema)).toThrowError('email field is not of type ID'); @@ -239,6 +259,7 @@ test('ModelConnectionTransformer should fail if partial sort key is passed in co const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => transformer.transform(validSchema)).toThrowError( @@ -266,6 +287,7 @@ test('ModelConnectionTransformer should accept connection without sort key', () const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => transformer.transform(validSchema)).not.toThrowError(); @@ -291,6 +313,7 @@ test('ModelConnectionTransformer should fail if sort key type passed in does not const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => transformer.transform(validSchema)).toThrowError('email field is not of type ID'); @@ -316,6 +339,7 @@ test('ModelConnectionTransformer should fail if partition key type passed in doe const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, }); expect(() => transformer.transform(validSchema)).toThrowError('email field is not of type ID'); @@ -341,6 +365,7 @@ test('Test ModelConnectionTransformer for One-to-One getItem case.', () => { const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); @@ -374,6 +399,7 @@ test('Test ModelConnectionTransformer for One-to-Many query case.', () => { const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); @@ -411,6 +437,7 @@ test('Test ModelConnectionTransformer for bidirectional One-to-Many query case.' const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); @@ -457,6 +484,7 @@ test('Test ModelConnectionTransformer for One-to-Many query with a composite sor const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); @@ -496,6 +524,7 @@ test('Test ModelConnectionTransformer for One-to-Many query with a composite sor const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); @@ -536,6 +565,7 @@ test('Test ModelConnectionTransformer for One-to-One getItem with composite sort const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); @@ -579,6 +609,7 @@ test('Many-to-many without conflict resolution generates correct schema', () => const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, transformConfig: { Version: TRANSFORM_CURRENT_VERSION, }, @@ -619,6 +650,7 @@ test('Many-to-many with conflict resolution generates correct schema', () => { const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + featureFlags, transformConfig: { Version: TRANSFORM_CURRENT_VERSION, ResolverConfig: { diff --git a/packages/graphql-dynamodb-transformer/src/DynamoDBModelTransformer.ts b/packages/graphql-dynamodb-transformer/src/DynamoDBModelTransformer.ts index e1644c76192..87b178667b6 100644 --- a/packages/graphql-dynamodb-transformer/src/DynamoDBModelTransformer.ts +++ b/packages/graphql-dynamodb-transformer/src/DynamoDBModelTransformer.ts @@ -494,7 +494,13 @@ export class DynamoDBModelTransformer extends Transformer { this.generateModelXConnectionType(ctx, def); // Create the list resolver - const listResolver = this.resources.makeListResolver(def.name.value, listFieldNameOverride, isSyncEnabled, ctx.getQueryTypeName()); + const listResolver = this.resources.makeListResolver( + def.name.value, + ctx.featureFlags.getBoolean('improvePluralization'), + listFieldNameOverride, + isSyncEnabled, + ctx.getQueryTypeName(), + ); const resourceId = ResolverResourceIDs.DynamoDBListResolverResourceID(typeName); ctx.setResource(resourceId, listResolver); ctx.mapResourceToStack(typeName, resourceId); diff --git a/packages/graphql-dynamodb-transformer/src/__tests__/DynamoDBModelTransformer.test.ts b/packages/graphql-dynamodb-transformer/src/__tests__/DynamoDBModelTransformer.test.ts index b0c8026424f..57b829288ca 100644 --- a/packages/graphql-dynamodb-transformer/src/__tests__/DynamoDBModelTransformer.test.ts +++ b/packages/graphql-dynamodb-transformer/src/__tests__/DynamoDBModelTransformer.test.ts @@ -14,6 +14,20 @@ import { import { FeatureFlagProvider, GraphQLTransform, TRANSFORM_BASE_VERSION, TRANSFORM_CURRENT_VERSION } from 'graphql-transformer-core'; import { DynamoDBModelTransformer } from '../DynamoDBModelTransformer'; import { getBaseType } from 'graphql-transformer-common'; +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + if (name === 'validateTypeNameReservedWords') { + return false; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; test('Test DynamoDBModelTransformer validation happy case', () => { const validSchema = ` @@ -26,6 +40,7 @@ test('Test DynamoDBModelTransformer validation happy case', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -41,6 +56,7 @@ test('Test DynamoDBModelTransformer with query overrides', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -75,6 +91,7 @@ test('Test DynamoDBModelTransformer with mutation overrides', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -96,6 +113,7 @@ test('Test DynamoDBModelTransformer with only create mutations', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -124,6 +142,7 @@ test('Test DynamoDBModelTransformer with multiple model directives', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -170,6 +189,7 @@ test('Test DynamoDBModelTransformer with filter', () => { }`; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -202,6 +222,7 @@ test('Test DynamoDBModelTransformer with mutations set to null', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -221,6 +242,7 @@ test('Test DynamoDBModelTransformer with queries set to null', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -242,6 +264,7 @@ test('Test DynamoDBModelTransformer with subscriptions set to null', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -265,6 +288,7 @@ test('Test DynamoDBModelTransformer with queries and mutations set to null', () `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -292,6 +316,7 @@ test('Test DynamoDBModelTransformer with advanced subscriptions', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -337,6 +362,7 @@ test('Test DynamoDBModelTransformer with non-model types and enums', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -385,6 +411,7 @@ test('Test DynamoDBModelTransformer with mutation input overrides when mutations `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -419,6 +446,7 @@ test('Test DynamoDBModelTransformer with mutation input overrides when mutations `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -446,6 +474,7 @@ test('Test non model objects contain id as a type for fields', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -470,6 +499,7 @@ test('Test schema includes attribute enum when only queries specified', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, transformConfig: { Version: 5, }, @@ -489,6 +519,7 @@ test('Test only get does not generate superfluous input and filter types', () => `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, transformConfig: { Version: 5, }, @@ -508,6 +539,7 @@ test('Test timestamp parameters when generating resolvers and output schema', () `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const result = transformer.transform(validSchema); expect(result).toBeDefined(); @@ -529,6 +561,7 @@ test('Test resolver template not to auto generate createdAt and updatedAt when t `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const result = transformer.transform(validSchema); expect(result).toBeDefined(); @@ -550,6 +583,7 @@ test('Test create and update mutation input should have timestamps as nullable f `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const result = transformer.transform(validSchema); expect(result).toBeDefined(); @@ -569,6 +603,7 @@ test('Test not to include createdAt and updatedAt field when timestamps is set t `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const result = transformer.transform(validSchema); expect(result).toBeDefined(); @@ -602,6 +637,7 @@ test('DynamoDB transformer should add default primary key when not defined', () `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const result = transformer.transform(validSchema); expect(result).toBeDefined(); @@ -627,6 +663,7 @@ test('DynamoDB transformer should not add default primary key when ID is defined `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const result = transformer.transform(validSchema); expect(result).toBeDefined(); @@ -677,6 +714,7 @@ test('Schema should compile successfully when subscription is missing from schem `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -719,6 +757,7 @@ function transformerVersionSnapshot(version: number): string { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer()], + featureFlags, transformConfig: { Version: version, }, diff --git a/packages/graphql-dynamodb-transformer/src/__tests__/__snapshots__/DynamoDBModelTransformer.test.ts.snap b/packages/graphql-dynamodb-transformer/src/__tests__/__snapshots__/DynamoDBModelTransformer.test.ts.snap index 90ce484f6b1..cc9aa4d15d3 100644 --- a/packages/graphql-dynamodb-transformer/src/__tests__/__snapshots__/DynamoDBModelTransformer.test.ts.snap +++ b/packages/graphql-dynamodb-transformer/src/__tests__/__snapshots__/DynamoDBModelTransformer.test.ts.snap @@ -1264,7 +1264,7 @@ enum ModelAttributeTypes { type Query { getEntity(id: ID!): Entity - listEntitys(filter: ModelEntityFilterInput, limit: Int, nextToken: String): ModelEntityConnection + listEntities(filter: ModelEntityFilterInput, limit: Int, nextToken: String): ModelEntityConnection } " `; diff --git a/packages/graphql-dynamodb-transformer/src/resources.ts b/packages/graphql-dynamodb-transformer/src/resources.ts index cf5af051f5f..35322e35a0f 100644 --- a/packages/graphql-dynamodb-transformer/src/resources.ts +++ b/packages/graphql-dynamodb-transformer/src/resources.ts @@ -685,8 +685,14 @@ export class ResourceFactory { * TODO: actually fill out the right filter expression. This is a placeholder only. * @param type */ - public makeListResolver(type: string, nameOverride?: string, isSyncEnabled: boolean = false, queryTypeName: string = 'Query') { - const fieldName = nameOverride ? nameOverride : graphqlName('list' + plurality(toUpper(type))); + public makeListResolver( + type: string, + improvePluralization: boolean, + nameOverride?: string, + isSyncEnabled: boolean = false, + queryTypeName: string = 'Query', + ) { + const fieldName = nameOverride ? nameOverride : graphqlName('list' + plurality(toUpper(type), improvePluralization)); const requestVariable = 'ListRequest'; return new AppSync.Resolver({ ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), diff --git a/packages/graphql-elasticsearch-transformer/src/SearchableModelTransformer.ts b/packages/graphql-elasticsearch-transformer/src/SearchableModelTransformer.ts index 8ab736b93b0..48746de2b74 100644 --- a/packages/graphql-elasticsearch-transformer/src/SearchableModelTransformer.ts +++ b/packages/graphql-elasticsearch-transformer/src/SearchableModelTransformer.ts @@ -125,6 +125,7 @@ export class SearchableModelTransformer extends Transformer { nonKeywordFields, primaryKey, ctx.getQueryTypeName(), + ctx.featureFlags.getBoolean('improvePluralization'), searchFieldNameOverride, ctx.isProjectUsingDataStore(), ); diff --git a/packages/graphql-elasticsearch-transformer/src/__tests__/SearchableModelTransformer.test.ts b/packages/graphql-elasticsearch-transformer/src/__tests__/SearchableModelTransformer.test.ts index b32dac3d7da..7a47b2a2bd9 100644 --- a/packages/graphql-elasticsearch-transformer/src/__tests__/SearchableModelTransformer.test.ts +++ b/packages/graphql-elasticsearch-transformer/src/__tests__/SearchableModelTransformer.test.ts @@ -1,6 +1,17 @@ import { GraphQLTransform, TRANSFORM_CURRENT_VERSION, ConflictHandlerType } from 'graphql-transformer-core'; import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; import { SearchableModelTransformer } from '../SearchableModelTransformer'; +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 = ` @@ -13,6 +24,7 @@ test('Test SearchableModelTransformer validation happy case', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new SearchableModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -30,6 +42,7 @@ test('Test SearchableModelTransformer with query overrides', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new SearchableModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -47,6 +60,7 @@ test('Test SearchableModelTransformer with only create mutations', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new SearchableModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -70,6 +84,7 @@ test('Test SearchableModelTransformer with multiple model searchable directives' `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new SearchableModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -88,6 +103,7 @@ test('Test SearchableModelTransformer with sort fields', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new SearchableModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -108,6 +124,7 @@ test('SearchableModelTransformer with external versioning', () => { const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new SearchableModelTransformer()], + featureFlags, transformConfig: { Version: TRANSFORM_CURRENT_VERSION, ResolverConfig: { diff --git a/packages/graphql-elasticsearch-transformer/src/resources.ts b/packages/graphql-elasticsearch-transformer/src/resources.ts index c7dd78ac2e4..65c081d5153 100644 --- a/packages/graphql-elasticsearch-transformer/src/resources.ts +++ b/packages/graphql-elasticsearch-transformer/src/resources.ts @@ -437,10 +437,11 @@ export class ResourceFactory { nonKeywordFields: Expression[], primaryKey: string, queryTypeName: string, + improvePluralization: boolean, nameOverride?: string, includeVersion: boolean = false, ) { - const fieldName = nameOverride ? nameOverride : graphqlName('search' + plurality(toUpper(type))); + const fieldName = nameOverride ? nameOverride : graphqlName('search' + plurality(toUpper(type), improvePluralization)); return new AppSync.Resolver({ ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchDataSourceLogicalID, 'Name'), diff --git a/packages/graphql-key-transformer/src/__tests__/KeyTransformer.test.ts b/packages/graphql-key-transformer/src/__tests__/KeyTransformer.test.ts index 7463cf71a42..3102d6983dd 100644 --- a/packages/graphql-key-transformer/src/__tests__/KeyTransformer.test.ts +++ b/packages/graphql-key-transformer/src/__tests__/KeyTransformer.test.ts @@ -2,6 +2,20 @@ import { parse, InputObjectTypeDefinitionNode, DefinitionNode, DocumentNode, Kin import { GraphQLTransform, InvalidDirectiveError, SyncConfig, ConflictHandlerType, FeatureFlagProvider } from 'graphql-transformer-core'; import { KeyTransformer } from '../KeyTransformer'; import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + if (name === 'skipOverrideMutationInputTypes') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; test('Check KeyTransformer Resolver Code', () => { const validSchema = ` @@ -20,6 +34,7 @@ test('Check KeyTransformer Resolver Code', () => { }`; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -35,6 +50,7 @@ test('KeyTransformer should fail if more than 1 @key is provided without a name. const transformer = new GraphQLTransform({ transformers: [new KeyTransformer()], + featureFlags, }); expect(() => transformer.transform(invalidSchema)).toThrowError(InvalidDirectiveError); @@ -50,6 +66,7 @@ test('KeyTransformer should fail if more than 1 @key is provided with the same n const transformer = new GraphQLTransform({ transformers: [new KeyTransformer()], + featureFlags, }); expect(() => transformer.transform(invalidSchema)).toThrowError(InvalidDirectiveError); @@ -65,6 +82,7 @@ test('KeyTransformer should fail if referencing a field that does not exist.', ( const transformer = new GraphQLTransform({ transformers: [new KeyTransformer()], + featureFlags, }); expect(() => transformer.transform(invalidSchema)).toThrowError(InvalidDirectiveError); @@ -80,6 +98,7 @@ test('Test that a primary @key fails if pointing to nullable fields.', () => { const transformer = new GraphQLTransform({ transformers: [new KeyTransformer()], + featureFlags, }); expect(() => transformer.transform(invalidSchema)).toThrowError(InvalidDirectiveError); @@ -95,6 +114,7 @@ test('Test that model with an LSI but no primary sort key will fail.', () => { const transformer = new GraphQLTransform({ transformers: [new KeyTransformer()], + featureFlags, }); expect(() => transformer.transform(invalidSchema)).toThrowError(InvalidDirectiveError); }); @@ -109,6 +129,7 @@ test('KeyTransformer should fail if a non-existing type field is defined as key const transformer = new GraphQLTransform({ transformers: [new KeyTransformer()], + featureFlags, }); expect(() => transformer.transform(invalidSchema)).toThrowError(InvalidDirectiveError); @@ -128,6 +149,7 @@ test('Check sortDirection validation code present in list resolver code for simp const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -149,6 +171,7 @@ test('Check sortDirection validation code present in list resolver code for comp const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -167,6 +190,7 @@ test('KeyTransformer should remove default primary key when primary key overidde const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -189,6 +213,7 @@ test('KeyTransformer should not remove default primary key when primary key not `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -223,6 +248,7 @@ test('Check KeyTransformer Resolver Code when sync enabled', () => { }; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], + featureFlags, transformConfig: { ResolverConfig: { project: config, @@ -249,6 +275,7 @@ test('Test that sort direction and filter input are generated if default list qu }`; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -286,6 +313,7 @@ test('GSI composite sort keys are wrapped in conditional to check presence in mu `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], + featureFlags, }); const result = transformer.transform(validSchema); @@ -300,20 +328,6 @@ function getInputType(doc: DocumentNode, type: string): InputObjectTypeDefinitio } describe('check schema input', () => { - let ff: FeatureFlagProvider; - beforeEach(() => { - ff = { - getBoolean: jest.fn().mockImplementation((name, defaultValue) => { - if (name === 'skipOverrideMutationInputTypes') { - return true; - } - }), - getNumber: jest.fn(), - getObject: jest.fn(), - getString: jest.fn(), - }; - }); - it('@model mutation with user defined null args ', () => { const validSchema = /* GraphQL */ ` type Call @@ -339,7 +353,7 @@ describe('check schema input', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], - featureFlags: ff, + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -376,7 +390,7 @@ describe('check schema input', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], - featureFlags: ff, + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -411,7 +425,7 @@ describe('check schema input', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], - featureFlags: ff, + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -457,7 +471,7 @@ describe('check schema input', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], - featureFlags: ff, + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); @@ -506,7 +520,7 @@ describe('check schema input', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], - featureFlags: ff, + featureFlags, }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); diff --git a/packages/graphql-relational-schema-transformer/src/RelationalDBResolverGenerator.ts b/packages/graphql-relational-schema-transformer/src/RelationalDBResolverGenerator.ts index d233db1d528..77c0eb57671 100644 --- a/packages/graphql-relational-schema-transformer/src/RelationalDBResolverGenerator.ts +++ b/packages/graphql-relational-schema-transformer/src/RelationalDBResolverGenerator.ts @@ -43,7 +43,7 @@ export class RelationalDBResolverGenerator { * Creates the CRUDL+Q Resolvers as a Map of Cloudform Resources. The output can then be * merged with an existing Template's map of Resources. */ - public createRelationalResolvers(resolverFilePath: string) { + public createRelationalResolvers(resolverFilePath: string, improvePluralization: boolean) { let resources = {}; this.resolverFilePath = resolverFilePath; this.typePrimaryKeyMap.forEach((value: string, key: string) => { @@ -54,7 +54,7 @@ export class RelationalDBResolverGenerator { ...{ [resourceName + 'GetResolver']: this.makeGetRelationalResolver(key) }, ...{ [resourceName + 'UpdateResolver']: this.makeUpdateRelationalResolver(key) }, ...{ [resourceName + 'DeleteResolver']: this.makeDeleteRelationalResolver(key) }, - ...{ [resourceName + 'ListResolver']: this.makeListRelationalResolver(key) }, + ...{ [resourceName + 'ListResolver']: this.makeListRelationalResolver(key, improvePluralization) }, }; // TODO: Add Guesstimate Query Resolvers }); @@ -306,8 +306,8 @@ export class RelationalDBResolverGenerator { * @param type - the graphql type for which the list resolver will be created * @param queryTypeName - will be 'Query' */ - private makeListRelationalResolver(type: string, queryTypeName: string = 'Query') { - const fieldName = graphqlName(GRAPHQL_RESOLVER_OPERATION.List + plurality(toUpper(type))); + private makeListRelationalResolver(type: string, improvePluralization: boolean, queryTypeName: string = 'Query') { + const fieldName = graphqlName(GRAPHQL_RESOLVER_OPERATION.List + plurality(toUpper(type), improvePluralization)); const selectSql = this.generateSelectStatement(type); const reqFileName = `${queryTypeName}.${fieldName}.req.vtl`; const resFileName = `${queryTypeName}.${fieldName}.res.vtl`; diff --git a/packages/graphql-relational-schema-transformer/src/RelationalDBTemplateGenerator.ts b/packages/graphql-relational-schema-transformer/src/RelationalDBTemplateGenerator.ts index 8074ad90342..bdff2625e80 100644 --- a/packages/graphql-relational-schema-transformer/src/RelationalDBTemplateGenerator.ts +++ b/packages/graphql-relational-schema-transformer/src/RelationalDBTemplateGenerator.ts @@ -47,9 +47,9 @@ export class RelationalDBTemplateGenerator { * @param template - the Cloudform template * @returns the given template, updated with new resolvers. */ - public addRelationalResolvers(template: Template, resolverFilePath: string): Template { + public addRelationalResolvers(template: Template, resolverFilePath: string, improvePluralization: boolean): Template { let resolverGenerator = new RelationalDBResolverGenerator(this.context); - template.Resources = { ...template.Resources, ...resolverGenerator.createRelationalResolvers(resolverFilePath) }; + template.Resources = { ...template.Resources, ...resolverGenerator.createRelationalResolvers(resolverFilePath, improvePluralization) }; return template; } diff --git a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBResolverGenerator.test.ts b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBResolverGenerator.test.ts index ed9e58961ca..41ff1cf57fe 100644 --- a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBResolverGenerator.test.ts +++ b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBResolverGenerator.test.ts @@ -1,6 +1,4 @@ import * as fs from 'fs-extra'; - -import { JSONMappingParameters } from 'cloudform-types/types/kinesisAnalyticsV2/applicationReferenceDataSource'; import { RelationalDBResolverGenerator } from '../RelationalDBResolverGenerator'; import { TemplateContext } from '../RelationalDBSchemaTransformer'; import { parse } from 'graphql'; @@ -8,7 +6,7 @@ import { parse } from 'graphql'; jest.mock('fs-extra', () => ({ writeFileSync: jest.fn(), })); - +const writeFileSync_mock = fs.writeFileSync as jest.MockedFunction; afterEach(() => jest.clearAllMocks()); /** @@ -41,7 +39,7 @@ test('Test Basic CRUDL Resolver Generation', () => { const generator = new RelationalDBResolverGenerator(context); // TEST - const resources: { [key: string]: any } = generator.createRelationalResolvers('someFilePath'); + const resources: { [key: string]: any } = generator.createRelationalResolvers('someFilePath', true); // VERIFY expect(resources).toBeDefined(); @@ -79,9 +77,9 @@ test('verify generated templates', () => { simplePrimaryKeyTypeMap.set('Tomatoes', 'String'); const context = new TemplateContext(schema, simplePrimaryKeyMap, simpleStringFieldMap, simpleIntFieldMap, simplePrimaryKeyTypeMap); const generator = new RelationalDBResolverGenerator(context); - generator.createRelationalResolvers('testFilePath'); - expect(fs.writeFileSync.mock.calls.length).toBe(10); - fs.writeFileSync.mock.calls.forEach(call => { + generator.createRelationalResolvers('testFilePath', true); + expect(writeFileSync_mock.mock.calls.length).toBe(10); + writeFileSync_mock.mock.calls.forEach(call => { expect(call.length).toBe(3); expect(call[0]).toMatchSnapshot(); expect(call[1]).toMatchSnapshot(); @@ -106,9 +104,36 @@ test('verify generated templates using a Int primary key', () => { simplePrimaryKeyTypeMap.set('Apples', 'Int'); const context = new TemplateContext(schema, simplePrimaryKeyMap, simpleStringFieldMap, simpleIntFieldMap, simplePrimaryKeyTypeMap); const generator = new RelationalDBResolverGenerator(context); - generator.createRelationalResolvers('testFilePath'); - expect(fs.writeFileSync.mock.calls.length).toBe(10); - fs.writeFileSync.mock.calls.forEach(call => { + generator.createRelationalResolvers('testFilePath', true); + expect(writeFileSync_mock.mock.calls.length).toBe(10); + writeFileSync_mock.mock.calls.forEach(call => { + expect(call.length).toBe(3); + expect(call[0]).toMatchSnapshot(); + expect(call[1]).toMatchSnapshot(); + expect(call[2]).toBe('utf8'); + }); +}); + +test('verify generated templates with old pluralization', () => { + // SETUP + const schema = parse(` + type Apples { + id: Int + name: String + } + `); + let simpleStringFieldMap = new Map(); + let simpleIntFieldMap = new Map(); + let simplePrimaryKeyMap = new Map(); + let simplePrimaryKeyTypeMap = new Map(); + + simplePrimaryKeyMap.set('Apples', 'Id'); + simplePrimaryKeyTypeMap.set('Apples', 'Int'); + const context = new TemplateContext(schema, simplePrimaryKeyMap, simpleStringFieldMap, simpleIntFieldMap, simplePrimaryKeyTypeMap); + const generator = new RelationalDBResolverGenerator(context); + generator.createRelationalResolvers('testFilePath', false); + expect(writeFileSync_mock.mock.calls.length).toBe(10); + writeFileSync_mock.mock.calls.forEach(call => { expect(call.length).toBe(3); expect(call[0]).toMatchSnapshot(); expect(call[1]).toMatchSnapshot(); diff --git a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBTemplateGenerator.test.ts b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBTemplateGenerator.test.ts index 242050ec6d3..aac31eb06a1 100644 --- a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBTemplateGenerator.test.ts +++ b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBTemplateGenerator.test.ts @@ -67,7 +67,7 @@ test('Test Adding Resolvers to CloudForm Template', () => { expect(baseTemplate.Resources).toBeDefined(); expect(baseTemplate.Resources).not.toHaveProperty(''); - const finalTemplate = templateGenerator.addRelationalResolvers(baseTemplate, 'someFilePath'); + const finalTemplate = templateGenerator.addRelationalResolvers(baseTemplate, 'someFilePath', true); expect(finalTemplate).toBeDefined(); }); diff --git a/packages/graphql-relational-schema-transformer/src/__tests__/__snapshots__/RelationalDBResolverGenerator.test.ts.snap b/packages/graphql-relational-schema-transformer/src/__tests__/__snapshots__/RelationalDBResolverGenerator.test.ts.snap index 83c0937907e..56f2eef59b9 100644 --- a/packages/graphql-relational-schema-transformer/src/__tests__/__snapshots__/RelationalDBResolverGenerator.test.ts.snap +++ b/packages/graphql-relational-schema-transformer/src/__tests__/__snapshots__/RelationalDBResolverGenerator.test.ts.snap @@ -95,7 +95,7 @@ exports[`verify generated templates 16`] = ` $utils.toJson($output[0])" `; -exports[`verify generated templates 17`] = `"testFilePath/Query.listTomatoess.req.vtl"`; +exports[`verify generated templates 17`] = `"testFilePath/Query.listTomatoes.req.vtl"`; exports[`verify generated templates 18`] = ` "{ @@ -104,7 +104,7 @@ exports[`verify generated templates 18`] = ` }" `; -exports[`verify generated templates 19`] = `"testFilePath/Query.listTomatoess.res.vtl"`; +exports[`verify generated templates 19`] = `"testFilePath/Query.listTomatoes.res.vtl"`; exports[`verify generated templates 20`] = `"$utils.toJson($utils.rds.toJsonObject($ctx.result)[0])"`; @@ -203,7 +203,7 @@ exports[`verify generated templates using a Int primary key 16`] = ` $utils.toJson($output[0])" `; -exports[`verify generated templates using a Int primary key 17`] = `"testFilePath/Query.listAppless.req.vtl"`; +exports[`verify generated templates using a Int primary key 17`] = `"testFilePath/Query.listApples.req.vtl"`; exports[`verify generated templates using a Int primary key 18`] = ` "{ @@ -212,6 +212,114 @@ exports[`verify generated templates using a Int primary key 18`] = ` }" `; -exports[`verify generated templates using a Int primary key 19`] = `"testFilePath/Query.listAppless.res.vtl"`; +exports[`verify generated templates using a Int primary key 19`] = `"testFilePath/Query.listApples.res.vtl"`; exports[`verify generated templates using a Int primary key 20`] = `"$utils.toJson($utils.rds.toJsonObject($ctx.result)[0])"`; + +exports[`verify generated templates with old pluralization 1`] = `"testFilePath/Mutation.createApples.req.vtl"`; + +exports[`verify generated templates with old pluralization 2`] = ` +"#set( $cols = [] ) +#set( $vals = [] ) +#foreach( $entry in $ctx.args.createApplesInput.keySet() ) + #set( $discard = $cols.add($entry) ) + #set( $discard = $vals.add(\\"'$ctx.args.createApplesInput[$entry]'\\") ) +#end +#set( $valStr = $vals.toString().replace(\\"[\\",\\"(\\").replace(\\"]\\",\\")\\") ) +#set( $colStr = $cols.toString().replace(\\"[\\",\\"(\\").replace(\\"]\\",\\")\\") ) +{ + \\"version\\": \\"2018-05-29\\", + \\"statements\\": [\\"INSERT INTO Apples $colStr VALUES $valStr\\", \\"SELECT * FROM Apples WHERE Id=$ctx.args.createApplesInput.Id\\"] +}" +`; + +exports[`verify generated templates with old pluralization 3`] = `"testFilePath/Mutation.createApples.res.vtl"`; + +exports[`verify generated templates with old pluralization 4`] = `"$utils.toJson($utils.parseJson($utils.rds.toJsonString($ctx.result))[1][0])"`; + +exports[`verify generated templates with old pluralization 5`] = `"testFilePath/Query.getApples.req.vtl"`; + +exports[`verify generated templates with old pluralization 6`] = ` +"{ + \\"version\\": \\"2018-05-29\\", + \\"statements\\": [\\"SELECT * FROM Apples WHERE Id=$ctx.args.Id\\"] +}" +`; + +exports[`verify generated templates with old pluralization 7`] = `"testFilePath/Query.getApples.res.vtl"`; + +exports[`verify generated templates with old pluralization 8`] = ` +"#set( $output = $utils.rds.toJsonObject($ctx.result) ) +#if( $output.isEmpty() ) + $util.error(\\"Invalid response from RDS DataSource. See info for the full response.\\", \\"InvalidResponse\\", {}, $output) +#end +#set( $output = $output[0] ) +#if( $output.isEmpty() ) + #return +#end +$utils.toJson($output[0])" +`; + +exports[`verify generated templates with old pluralization 9`] = `"testFilePath/Mutation.updateApples.req.vtl"`; + +exports[`verify generated templates with old pluralization 10`] = ` +"#set( $updateList = {} ) +#foreach( $entry in $ctx.args.updateApplesInput.keySet() ) + #set( $discard = $updateList.put($entry, \\"'$ctx.args.updateApplesInput[$entry]'\\") ) +#end +#set( $update = $updateList.toString().replace(\\"{\\",\\"\\").replace(\\"}\\",\\"\\") ) +{ + \\"version\\": \\"2018-05-29\\", + \\"statements\\": [\\"UPDATE Apples SET $update WHERE Id=$ctx.args.updateApplesInput.Id}\\", \\"SELECT * FROM Apples WHERE Id=$ctx.args.updateApplesInput.Id\\"] +}" +`; + +exports[`verify generated templates with old pluralization 11`] = `"testFilePath/Mutation.updateApples.res.vtl"`; + +exports[`verify generated templates with old pluralization 12`] = ` +"#set( $output = $utils.rds.toJsonObject($ctx.result) ) +#if( $output.length() < 2 ) + $util.error(\\"Invalid response from RDS DataSource. See info for the full response.\\", \\"InvalidResponse\\", {}, $output) +#end +#set( $output = $output[1] ) +#if( $output.isEmpty() ) + #return +#end +$utils.toJson($output[0])" +`; + +exports[`verify generated templates with old pluralization 13`] = `"testFilePath/Mutation.deleteApples.req.vtl"`; + +exports[`verify generated templates with old pluralization 14`] = ` +"{ + \\"version\\": \\"2018-05-29\\", + \\"statements\\": [\\"SELECT * FROM Apples WHERE Id=$ctx.args.Id\\", \\"DELETE FROM Apples WHERE Id=$ctx.args.Id\\"] +}" +`; + +exports[`verify generated templates with old pluralization 15`] = `"testFilePath/Mutation.deleteApples.res.vtl"`; + +exports[`verify generated templates with old pluralization 16`] = ` +"#set( $output = $utils.rds.toJsonObject($ctx.result) ) +#if( $output.isEmpty() ) + $util.error(\\"Invalid response from RDS DataSource. See info for the full response.\\", \\"InvalidResponse\\", {}, $output) +#end +#set( $output = $output[0] ) +#if( $output.isEmpty() ) + #return +#end +$utils.toJson($output[0])" +`; + +exports[`verify generated templates with old pluralization 17`] = `"testFilePath/Query.listAppless.req.vtl"`; + +exports[`verify generated templates with old pluralization 18`] = ` +"{ + \\"version\\": \\"2018-05-29\\", + \\"statements\\": [\\"SELECT * FROM Apples\\"] +}" +`; + +exports[`verify generated templates with old pluralization 19`] = `"testFilePath/Query.listAppless.res.vtl"`; + +exports[`verify generated templates with old pluralization 20`] = `"$utils.toJson($utils.rds.toJsonObject($ctx.result)[0])"`; diff --git a/packages/graphql-transformer-common/package.json b/packages/graphql-transformer-common/package.json index 445cb556867..32c109ce574 100644 --- a/packages/graphql-transformer-common/package.json +++ b/packages/graphql-transformer-common/package.json @@ -25,7 +25,8 @@ "dependencies": { "graphql": "^14.5.8", "graphql-mapping-template": "4.18.1", - "md5": "^2.2.1" + "md5": "^2.2.1", + "pluralize": "8.0.0" }, "devDependencies": { "@types/md5": "^2.1.33", diff --git a/packages/graphql-transformer-common/src/util.ts b/packages/graphql-transformer-common/src/util.ts index cca2086ee79..9e7c8a347c2 100644 --- a/packages/graphql-transformer-common/src/util.ts +++ b/packages/graphql-transformer-common/src/util.ts @@ -1,9 +1,14 @@ import md5 from 'md5'; - -export function plurality(val: string): string { +import pluralize from 'pluralize'; +export function plurality(val: string, improvePluralization: boolean): string { if (!val.trim()) { return ''; } + + if (improvePluralization) { + return pluralize(val); + } + return val.concat('s'); } diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/ConnectionsWithAuthTests.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/ConnectionsWithAuthTests.e2e.test.ts index d3ed116439d..2af1b3b923e 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/ConnectionsWithAuthTests.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/ConnectionsWithAuthTests.e2e.test.ts @@ -29,6 +29,17 @@ import 'isomorphic-fetch'; jest.setTimeout(2000000); const cf = new CloudFormationClient('us-west-2'); +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `ConnectionsWithAuthTests-${BUILD_TIMESTAMP}`; @@ -148,6 +159,7 @@ beforeAll(async () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelConnectionTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/CustomRoots.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/CustomRoots.e2e.test.ts index e522e4553ab..9c1b075b99a 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/CustomRoots.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/CustomRoots.e2e.test.ts @@ -14,6 +14,17 @@ import fs = require('fs'); import path = require('path'); jest.setTimeout(2000000); +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 custom root types with additional fields.', () => { const validSchema = ` @@ -32,6 +43,7 @@ test('Test custom root types with additional fields.', () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer()], }); const out = transformer.transform(validSchema); @@ -75,6 +87,7 @@ test('Test custom root query, mutation, and subscriptions.', () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer()], }); const out = transformer.transform(validSchema); @@ -115,6 +128,7 @@ test('Test custom roots without any directives. This should still be valid.', () } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer()], }); const out = transformer.transform(validSchema); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/DynamoDBModelTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/DynamoDBModelTransformer.e2e.test.ts index 2bbb1e1281f..48fa21cd15c 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/DynamoDBModelTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/DynamoDBModelTransformer.e2e.test.ts @@ -15,7 +15,17 @@ jest.setTimeout(2000000); const cf = new CloudFormationClient('us-west-2'); const customS3Client = new S3Client('us-west-2'); const awsS3Client = new S3({ region: 'us-west-2' }); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `DynamoDBModelTransformerTest-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `appsync-model-transformer-test-bucket-${BUILD_TIMESTAMP}`; @@ -81,6 +91,7 @@ beforeAll(async () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/FunctionTransformerTests.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/FunctionTransformerTests.e2e.test.ts index b3025ebc023..d58119e700c 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/FunctionTransformerTests.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/FunctionTransformerTests.e2e.test.ts @@ -18,7 +18,17 @@ jest.setTimeout(2000000); const cf = new CloudFormationClient('us-west-2'); const customS3Client = new S3Client('us-west-2'); const awsS3Client = new S3({ region: 'us-west-2' }); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `FunctionTransformerTests-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `appsync-function-transformer-test-bucket-${BUILD_TIMESTAMP}`; @@ -86,6 +96,7 @@ beforeAll(async () => { expect(true).toEqual(false); } const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new FunctionTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/HttpTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/HttpTransformer.e2e.test.ts index 4e6fe72cee7..dde1076aa1f 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/HttpTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/HttpTransformer.e2e.test.ts @@ -17,7 +17,17 @@ jest.setTimeout(2000000); const cf = new CloudFormationClient('us-west-2'); const customS3Client = new S3Client('us-west-2'); const awsS3Client = new S3({ region: 'us-west-2' }); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `HttpTransformerTest-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `appsync-http-transformer-test-bucket-${BUILD_TIMESTAMP}`; @@ -89,6 +99,7 @@ beforeAll(async () => { } const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformer.e2e.test.ts index 75dfdec8da4..bf29dbd3d49 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformer.e2e.test.ts @@ -16,7 +16,17 @@ jest.setTimeout(2000000); const cf = new CloudFormationClient('us-west-2'); const customS3Client = new S3Client('us-west-2'); const awsS3Client = new S3({ region: 'us-west-2' }); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `KeyTransformerTests-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `appsync-key-transformer-test-bucket-${BUILD_TIMESTAMP}`; @@ -95,6 +105,7 @@ beforeAll(async () => { console.warn(`Could not create bucket: ${e}`); } const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new KeyTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformerLocal.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformerLocal.e2e.test.ts index f1bf02a3f6e..32c8d630c5b 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformerLocal.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformerLocal.e2e.test.ts @@ -10,7 +10,17 @@ import { expectNullableInputValues, expectInputValueToHandle, } from '../testUtil'; - +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 that a primary @key with a single field changes the hash key.', () => { const validSchema = ` type Test @model @key(fields: ["email"]) { @@ -19,6 +29,7 @@ test('Test that a primary @key with a single field changes the hash key.', () => `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], }); @@ -44,6 +55,7 @@ test('Test that a primary @key with 2 fields changes the hash and sort key.', () `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], }); @@ -77,6 +89,7 @@ test('Test that a primary @key with id as hashKey does not have it required in c `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], }); @@ -104,6 +117,7 @@ test('Test that a primary @key with id and createdAt it is not a required in cre `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], }); @@ -131,6 +145,7 @@ test('Test that a primary @key with emailId and location makes it a required in `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], }); @@ -158,6 +173,7 @@ test('Test that a primary @key with 3 fields changes the hash and sort keys.', ( `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], }); @@ -196,6 +212,7 @@ test('Test that a secondary @key with 3 fields changes the hash and sort keys an `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], }); @@ -243,6 +260,7 @@ test('Test that a secondary @key with a single field adds a GSI.', () => { `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], }); @@ -277,6 +295,7 @@ test('Test that a secondary @key with a multiple field adds an GSI.', () => { `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], }); @@ -326,15 +345,16 @@ test('Test that a secondary @key with a multiple field adds an LSI with GSI FF t } `; - const transformer = new GraphQLTransform({ - transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], - featureFlags: { - getBoolean: (featureName: string, defaultValue: boolean) => { - if (featureName === 'secondaryKeyAsGSI') return false; - return defaultValue || false; - }, - } as unknown as FeatureFlagProvider - }); + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], + featureFlags: ({ + getBoolean: (featureName: string, defaultValue: boolean) => { + if (featureName === 'secondaryKeyAsGSI') return false; + if (featureName === 'improvePluralization') return true; + return defaultValue || false; + }, + } as unknown) as FeatureFlagProvider, + }); const out = transformer.transform(validSchema); let tableResource = out.stacks.Test.Resources.TestTable; @@ -369,15 +389,16 @@ test('Test that a secondary @key with a multiple field adds an GSI based on enab } `; - const transformer = new GraphQLTransform({ - transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], - featureFlags: { - getBoolean: (featureName: string, defaultValue: boolean) => { - if (featureName === 'secondaryKeyAsGSI') return true; - return defaultValue || false; - }, - } as unknown as FeatureFlagProvider - }); + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], + featureFlags: ({ + getBoolean: (featureName: string, defaultValue: boolean) => { + if (featureName === 'secondaryKeyAsGSI') return true; + if (featureName === 'improvePluralization') return true; + return defaultValue || false; + }, + } as unknown) as FeatureFlagProvider, + }); const out = transformer.transform(validSchema); let tableResource = out.stacks.Test.Resources.TestTable; @@ -412,6 +433,7 @@ test('Test that a primary @key with complex fields will update the input objects `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], }); @@ -475,6 +497,7 @@ test('Test that connection type is generated for custom query when queries is se `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer(), new KeyTransformer()], }); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/KeyWithAuth.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/KeyWithAuth.e2e.test.ts index 267bc3e9a1f..059bd5f6c57 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/KeyWithAuth.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/KeyWithAuth.e2e.test.ts @@ -29,7 +29,17 @@ import 'isomorphic-fetch'; jest.setTimeout(2000000); const cf = new CloudFormationClient('us-west-2'); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `KeyWithAuth-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `appsync-key-with-auth-test-bucket-${BUILD_TIMESTAMP}`; @@ -110,6 +120,7 @@ beforeAll(async () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new KeyTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/ModelAuthTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/ModelAuthTransformer.e2e.test.ts index 8ad8fd7ff6a..d027cbddc7c 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/ModelAuthTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/ModelAuthTransformer.e2e.test.ts @@ -28,7 +28,17 @@ import 'isomorphic-fetch'; (global as any).fetch = require('node-fetch'); jest.setTimeout(2000000); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; describe(`ModelAuthTests`, () => { const cf = new CloudFormationClient('us-west-2'); @@ -195,6 +205,7 @@ describe(`ModelAuthTests`, () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelConnectionTransformer(), @@ -918,7 +929,7 @@ describe(`ModelAuthTests`, () => { expect((req2.errors[0] as any).errorType).toEqual('Unauthorized'); }); - test(`Test listSalarys w/ Admin group protection authorized`, async () => { + test(`Test listSalaries w/ Admin group protection authorized`, async () => { const req = await GRAPHQL_CLIENT_1.query( ` mutation { @@ -936,7 +947,7 @@ describe(`ModelAuthTests`, () => { const req2 = await GRAPHQL_CLIENT_1.query( ` query { - listSalarys(filter: { wage: { eq: 101 }}) { + listSalaries(filter: { wage: { eq: 101 }}) { items { id wage @@ -946,12 +957,12 @@ describe(`ModelAuthTests`, () => { `, {}, ); - expect(req2.data.listSalarys.items.length).toEqual(1); - expect(req2.data.listSalarys.items[0].id).toEqual(req.data.createSalary.id); - expect(req2.data.listSalarys.items[0].wage).toEqual(101); + expect(req2.data.listSalaries.items.length).toEqual(1); + expect(req2.data.listSalaries.items[0].id).toEqual(req.data.createSalary.id); + expect(req2.data.listSalaries.items[0].wage).toEqual(101); }); - test(`Test listSalarys w/ Admin group protection not authorized`, async () => { + test(`Test listSalaries w/ Admin group protection not authorized`, async () => { const req = await GRAPHQL_CLIENT_1.query( ` mutation { @@ -969,7 +980,7 @@ describe(`ModelAuthTests`, () => { const req2 = await GRAPHQL_CLIENT_2.query( ` query { - listSalarys(filter: { wage: { eq: 102 }}) { + listSalaries(filter: { wage: { eq: 102 }}) { items { id wage @@ -979,7 +990,7 @@ describe(`ModelAuthTests`, () => { `, {}, ); - expect(req2.data.listSalarys.items).toEqual([]); + expect(req2.data.listSalaries.items).toEqual([]); }); /** @@ -2817,7 +2828,7 @@ describe(`ModelAuthTests`, () => { const listResponse = await GRAPHQL_CLIENT_3.query( `query { - listTestIdentitys(filter: { title: { eq: "Test title update" } }, limit: 100) { + listTestIdentities(filter: { title: { eq: "Test title update" } }, limit: 100) { items { id title @@ -2827,7 +2838,7 @@ describe(`ModelAuthTests`, () => { }`, {}, ); - const relevantPost = listResponse.data.listTestIdentitys.items.find(p => p.id === getReq.data.getTestIdentity.id); + const relevantPost = listResponse.data.listTestIdentities.items.find(p => p.id === getReq.data.getTestIdentity.id); expect(relevantPost).toBeTruthy(); expect(relevantPost.title).toEqual('Test title update'); expect(relevantPost.owner.slice(0, 19)).toEqual('https://cognito-idp'); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionTransformer.e2e.test.ts index 23824f4044f..ec30f5ab3f3 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionTransformer.e2e.test.ts @@ -16,7 +16,17 @@ jest.setTimeout(2000000); const cf = new CloudFormationClient('us-west-2'); const customS3Client = new S3Client('us-west-2'); const awsS3Client = new S3({ region: 'us-west-2' }); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `ModelConnectionTransformerTest-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `appsync-connection-transformer-test-${BUILD_TIMESTAMP}`; @@ -66,6 +76,7 @@ beforeAll(async () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelConnectionTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionWithKeyTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionWithKeyTransformer.e2e.test.ts index 8267c25dbeb..fd19b3911cf 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionWithKeyTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionWithKeyTransformer.e2e.test.ts @@ -17,7 +17,17 @@ jest.setTimeout(2000000); const cf = new CloudFormationClient('us-west-2'); const customS3Client = new S3Client('us-west-2'); const awsS3Client = new S3({ region: 'us-west-2' }); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `ModelConnectionKeyTransformerTest-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `appsync-connection-key-transformer-test-${BUILD_TIMESTAMP}`; @@ -146,6 +156,7 @@ beforeAll(async () => { `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new KeyTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/MultiAuthModelAuthTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/MultiAuthModelAuthTransformer.e2e.test.ts index 146f7480209..9e11842052a 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/MultiAuthModelAuthTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/MultiAuthModelAuthTransformer.e2e.test.ts @@ -38,7 +38,17 @@ jest.setTimeout(2000000); const REGION = 'us-west-2'; const cf = new CloudFormationClient(REGION); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `MultiAuthModelAuthTransformerTest-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `appsync-multi-auth-transformer-test-bucket-${BUILD_TIMESTAMP}`; @@ -238,6 +248,7 @@ beforeAll(async () => { `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelConnectionTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/MutationCondition.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/MutationCondition.e2e.test.ts index a27490b2f60..7558bd1354f 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/MutationCondition.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/MutationCondition.e2e.test.ts @@ -34,9 +34,20 @@ import AWS = require('aws-sdk'); import 'isomorphic-fetch'; jest.setTimeout(2000000); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const transformAndParseSchema = (schema: string, version: number = TRANSFORM_CURRENT_VERSION): DocumentNode => { const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new VersionedModelTransformer(), @@ -604,6 +615,7 @@ describe(`Deployed Mutation Condition tests`, () => { `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new VersionedModelTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/NestedStacksTest.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/NestedStacksTest.e2e.test.ts index 4797ae50205..051ecee72bb 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/NestedStacksTest.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/NestedStacksTest.e2e.test.ts @@ -14,7 +14,17 @@ import fs = require('fs'); import path = require('path'); jest.setTimeout(2000000); - +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 custom root types with additional fields.', () => { const validSchema = ` type Query { @@ -32,6 +42,7 @@ test('Test custom root types with additional fields.', () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [new DynamoDBModelTransformer()], }); // GetAttGraphQLAPIId diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionTransformer.e2e.test.ts index a36afb127e9..09ff17d1686 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionTransformer.e2e.test.ts @@ -17,7 +17,17 @@ jest.setTimeout(2000000); const cf = new CloudFormationClient('us-west-2'); const customS3Client = new S3Client('us-west-2'); const awsS3Client = new S3({ region: 'us-west-2' }); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `NewConnectionTransformerTest-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `appsync-new-connection-transformer-test-${BUILD_TIMESTAMP}`; @@ -124,6 +134,7 @@ type PostAuthor let out: DeploymentResources = undefined; try { const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new KeyTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionWithAuth.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionWithAuth.e2e.test.ts index ba0a2be1d42..49fcdd847c4 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionWithAuth.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionWithAuth.e2e.test.ts @@ -30,7 +30,17 @@ import 'isomorphic-fetch'; jest.setTimeout(2000000); const cf = new CloudFormationClient('us-west-2'); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `NewConnectionsWithAuthTests-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `new-connections-with-auth-test-bucket-${BUILD_TIMESTAMP}`; @@ -141,6 +151,7 @@ beforeAll(async () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new KeyTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/NonModelAuthFunction.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/NonModelAuthFunction.e2e.test.ts index 645ab7364c2..b9a6e023885 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/NonModelAuthFunction.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/NonModelAuthFunction.e2e.test.ts @@ -32,7 +32,17 @@ import { AuthenticationDetails } from 'amazon-cognito-identity-js'; // to deal with bug in cognito-identity-js (global as any).fetch = require('node-fetch'); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; // To overcome of the way of how AmplifyJS picks up currentUserCredentials const anyAWS = AWS as any; @@ -113,6 +123,7 @@ beforeAll(async () => { console.warn(`Could not setup function: ${e}`); } const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new FunctionTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/NoneEnvFunctionTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/NoneEnvFunctionTransformer.e2e.test.ts index be0cd94b53f..51e33e36fdc 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/NoneEnvFunctionTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/NoneEnvFunctionTransformer.e2e.test.ts @@ -18,7 +18,17 @@ jest.setTimeout(2000000); const cf = new CloudFormationClient('us-west-2'); const customS3Client = new S3Client('us-west-2'); const awsS3Client = new S3({ region: 'us-west-2' }); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `NoneEnvFunctionTransformerTests-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `appsync-none-env-func-trans-test-bucket-${BUILD_TIMESTAMP}`; @@ -74,6 +84,7 @@ beforeAll(async () => { console.warn(`Could not setup function: ${e}`); } const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new FunctionTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/PerFieldAuthTests.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/PerFieldAuthTests.e2e.test.ts index f95f1aa8eee..bbb7f70e3a8 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/PerFieldAuthTests.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/PerFieldAuthTests.e2e.test.ts @@ -29,7 +29,17 @@ import 'isomorphic-fetch'; jest.setTimeout(2000000); const cf = new CloudFormationClient('us-west-2'); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `PerFieldAuthTests-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `per-field-auth-tests-bucket-${BUILD_TIMESTAMP}`; @@ -165,6 +175,7 @@ beforeAll(async () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelConnectionTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/PredictionsTransformerTests.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/PredictionsTransformerTests.e2e.test.ts index c9c866c85bb..1376a05a935 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/PredictionsTransformerTests.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/PredictionsTransformerTests.e2e.test.ts @@ -18,7 +18,17 @@ const AWS_REGION = 'us-east-2'; const cf = new CloudFormationClient(AWS_REGION); const customS3Client = new S3Client(AWS_REGION); const awsS3Client = new S3({ region: AWS_REGION }); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `PredictionsTransformerTests-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `appsync-predictions-transformer-test-bucket-${BUILD_TIMESTAMP}`; @@ -48,6 +58,7 @@ beforeAll(async () => { console.warn(`Could not create bucket: ${e}`); } const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new PredictionsTransformer({ bucketName: BUCKET_NAME }), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableModelTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableModelTransformer.e2e.test.ts index 73005341825..65c5dad5571 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableModelTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableModelTransformer.e2e.test.ts @@ -20,6 +20,17 @@ const cf = new CloudFormationClient('us-west-2'); const customS3Client = new S3Client('us-west-2'); const awsS3Client = new S3({ region: 'us-west-2' }); let GRAPHQL_CLIENT: GraphQLClient = undefined; +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `TestSearchableModelTransformer-${BUILD_TIMESTAMP}`; @@ -127,6 +138,7 @@ beforeAll(async () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new KeyTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableWithAuthTests.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableWithAuthTests.e2e.test.ts index 6d9273c19b4..298e0a62d91 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableWithAuthTests.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableWithAuthTests.e2e.test.ts @@ -30,7 +30,17 @@ import 'isomorphic-fetch'; // To overcome of the way of how AmplifyJS picks up currentUserCredentials const anyAWS = AWS as any; - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; if (anyAWS && anyAWS.config && anyAWS.config.credentials) { delete anyAWS.config.credentials; } @@ -152,6 +162,7 @@ beforeAll(async () => { }`; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelConnectionTransformer(), diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/SubscriptionsWithAuthTest.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/SubscriptionsWithAuthTest.e2e.test.ts index 8ab3291167f..63986efe9f0 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/SubscriptionsWithAuthTest.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/SubscriptionsWithAuthTest.e2e.test.ts @@ -37,7 +37,17 @@ const anyAWS = AWS as any; if (anyAWS && anyAWS.config && anyAWS.config.credentials) { delete anyAWS.config.credentials; } - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; // to deal with bug in cognito-identity-js (global as any).fetch = require('node-fetch'); // to deal with subscriptions in node env @@ -240,6 +250,7 @@ beforeAll(async () => { }`; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new ModelAuthTransformer({ diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/TestComplexStackMappingsLocal.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/TestComplexStackMappingsLocal.e2e.test.ts index 905d4edc05d..36f963d5995 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/TestComplexStackMappingsLocal.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/TestComplexStackMappingsLocal.e2e.test.ts @@ -29,6 +29,17 @@ type Post @model @searchable { score: Int @function(name: "scorefunc") } `; +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; /** * We test this schema with the same set of rules multiple times. This protects against a subtle bug in the stack mapping @@ -51,6 +62,7 @@ test('Test that every resource exists in the correct stack given a complex schem function transpileAndCheck(schema: string) { const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new HttpTransformer(), @@ -85,7 +97,7 @@ function transpileAndCheck(schema: string) { 'FunctionDirectiveStack', 'HttpStack', 'NoneDataSource', - ]) + ]), ); expectExactKeys(out.rootStack.Outputs, new Set(['GraphQLAPIIdOutput', 'GraphQLAPIEndpointOutput', 'GraphQLAPIKeyOutput'])); @@ -104,7 +116,7 @@ function transpileAndCheck(schema: string) { 'SubscriptiononCreateUserResolver', 'SubscriptiononDeleteUserResolver', 'SubscriptiononUpdateUserResolver', - ]) + ]), ); expectExactKeys(out.stacks.User.Outputs, new Set(['GetAttUserTableStreamArn', 'GetAttUserDataSourceName', 'GetAttUserTableName'])); @@ -120,11 +132,11 @@ function transpileAndCheck(schema: string) { 'CreateUserPostResolver', 'UpdateUserPostResolver', 'DeleteUserPostResolver', - ]) + ]), ); expectExactKeys( out.stacks.UserPost.Outputs, - new Set(['GetAttUserPostTableStreamArn', 'GetAttUserPostDataSourceName', 'GetAttUserPostTableName']) + new Set(['GetAttUserPostTableStreamArn', 'GetAttUserPostDataSourceName', 'GetAttUserPostTableName']), ); // Check Post @@ -139,7 +151,7 @@ function transpileAndCheck(schema: string) { 'CreatePostResolver', 'UpdatePostResolver', 'DeletePostResolver', - ]) + ]), ); expectExactKeys(out.stacks.Post.Outputs, new Set(['GetAttPostTableStreamArn', 'GetAttPostDataSourceName', 'GetAttPostTableName'])); @@ -154,21 +166,21 @@ function transpileAndCheck(schema: string) { 'ElasticSearchStreamingLambdaFunction', 'SearchablePostLambdaMapping', 'SearchPostResolver', - ]) + ]), ); expectExactKeys(out.stacks.SearchableStack.Outputs, new Set(['ElasticsearchDomainArn', 'ElasticsearchDomainEndpoint'])); // Check connections expectExactKeys( out.stacks.ConnectionStack.Resources, - new Set(['UserpostsResolver', 'UserPostuserResolver', 'UserPostpostResolver', 'PostauthorsResolver']) + new Set(['UserpostsResolver', 'UserPostuserResolver', 'UserPostpostResolver', 'PostauthorsResolver']), ); expectExactKeys(out.stacks.ConnectionStack.Outputs, new Set([])); // Check function stack expectExactKeys( out.stacks.FunctionDirectiveStack.Resources, - new Set(['ScorefuncLambdaDataSourceRole', 'ScorefuncLambdaDataSource', 'InvokeScorefuncLambdaDataSource', 'PostscoreResolver']) + new Set(['ScorefuncLambdaDataSourceRole', 'ScorefuncLambdaDataSource', 'InvokeScorefuncLambdaDataSource', 'PostscoreResolver']), ); expectExactKeys(out.stacks.ConnectionStack.Outputs, new Set([])); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/VersionedModelTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/VersionedModelTransformer.e2e.test.ts index 1ef8371abb9..c55267c033a 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/VersionedModelTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/VersionedModelTransformer.e2e.test.ts @@ -14,7 +14,17 @@ import { cleanupStackAfterTest, deploy } from '../deployNestedStacks'; jest.setTimeout(2000000); const cf = new CloudFormationClient('us-west-2'); - +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); const STACK_NAME = `VersionedTest-${BUILD_TIMESTAMP}`; const BUCKET_NAME = `versioned-test-bucket-${BUILD_TIMESTAMP}`; @@ -44,6 +54,7 @@ beforeAll(async () => { } `; const transformer = new GraphQLTransform({ + featureFlags, transformers: [ new DynamoDBModelTransformer(), new VersionedModelTransformer(), diff --git a/packages/graphql-versioned-transformer/src/__tests__/VersionedModelTransformer.test.ts b/packages/graphql-versioned-transformer/src/__tests__/VersionedModelTransformer.test.ts index fdfd9915cdd..2d3f5efa3ea 100644 --- a/packages/graphql-versioned-transformer/src/__tests__/VersionedModelTransformer.test.ts +++ b/packages/graphql-versioned-transformer/src/__tests__/VersionedModelTransformer.test.ts @@ -2,13 +2,23 @@ import { ObjectTypeDefinitionNode, parse, DocumentNode, Kind, InputObjectTypeDef import { GraphQLTransform } from 'graphql-transformer-core'; import { VersionedModelTransformer } from '../VersionedModelTransformer'; import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; - const getInputType = (schemaDoc: DocumentNode) => (name: string): InputObjectTypeDefinitionNode => schemaDoc.definitions.find(d => d.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && d.name.value === name) as InputObjectTypeDefinitionNode; const getInputField = (input: InputObjectTypeDefinitionNode, field: string) => input.fields.find(f => f.name.value === field); const getType = (schemaDoc: DocumentNode) => (name: string): ObjectTypeDefinitionNode => schemaDoc.definitions.find(d => d.kind === Kind.OBJECT_TYPE_DEFINITION && d.name.value === name) as ObjectTypeDefinitionNode; const getField = (input: ObjectTypeDefinitionNode, field: string) => input.fields.find(f => f.name.value === field); +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 VersionedModelTransformer validation happy case', () => { const validSchema = ` @@ -21,6 +31,7 @@ test('Test VersionedModelTransformer validation happy case', () => { `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new VersionedModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); // tslint:disable-next-line @@ -47,6 +58,7 @@ test('Test VersionedModelTransformer validation fails when provided version fiel try { const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new VersionedModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); } catch (e) { @@ -66,6 +78,7 @@ test('Test VersionedModelTransformer version field replaced by non-null if provi `; const transformer = new GraphQLTransform({ transformers: [new DynamoDBModelTransformer(), new VersionedModelTransformer()], + featureFlags, }); const out = transformer.transform(validSchema); const sdl = out.schema; diff --git a/yarn.lock b/yarn.lock index 7abee12c951..31530056264 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19422,7 +19422,7 @@ plist@^3.0.1: xmlbuilder "^9.0.7" xmldom "0.1.x" -pluralize@^8.0.0: +pluralize@8.0.0, pluralize@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==