Skip to content

Commit cf95f9c

Browse files
authored
feat(appsync-modelgen-plugin): Many to many directive (#238)
* Feature: Add new @PrimaryKey and @index directives Added support for the java visitor, swift and process-connection up next * Feature: swift visitor processes @PrimaryKey and @index Fix the location of the java visitor primaryKey/index tests to sit next to key * Feature: Adding @key replacement support to process-connections WIP * Feature: Adding @key replacement support to process-connections primaryKey and index are supported directives for connections unit tests are added We'll need to remove the side by side support for the v1 and v2 transformers post-release * Fix: add feature flags mocking to other unit tests * Fix: Update the way version is calculated for vNext transformer * Feature: WIP addition of hasOne directive * Fix: Get feature flag according to codegen pattern * Feature: WIP addition of hasOne directive * primaryKey/index directives (#217) * Feature: Add new @PrimaryKey and @index directives Added support for the java visitor, swift and process-connection up next * Feature: swift visitor processes @PrimaryKey and @index Fix the location of the java visitor primaryKey/index tests to sit next to key * Feature: Adding @key replacement support to process-connections WIP * Feature: Adding @key replacement support to process-connections primaryKey and index are supported directives for connections unit tests are added We'll need to remove the side by side support for the v1 and v2 transformers post-release * Fix: add feature flags mocking to other unit tests * Fix: Update the way version is calculated for vNext transformer * Fix: Get feature flag according to codegen pattern * WIP Feature: V2 connection directive replacements hasOne, hasMany, belongsTo manyToMany is to come in a future branch * Fix: Purge FeatureFlags cli core usage, update tests * Add another unit test for hasOne * Fix all the merge conflicts I already resolved but git re-broke * Fix: Process connections test suite Match the scenarios in the design doc, cover another case * WIP Change: process manyToMany directive into composing hasMany and belongsTo * Fix: Remove or update some outdated code in connections processing * Fix: Code quality changes for LGTM bot * WIP Change: process manyToMany directive into composing hasMany and belongsTo * Fix: swift visitor index directive, unit test compare metadata * Revert "Fix: swift visitor index directive, unit test compare metadata" This reverts commit dd45b45. * Fix: remove unused import * Fix: Remove unreachable code and add some error checking belongsTo * Feature: Add new @PrimaryKey and @index directives Added support for the java visitor, swift and process-connection up next * Feature: swift visitor processes @PrimaryKey and @index Fix the location of the java visitor primaryKey/index tests to sit next to key * Fix: add feature flags mocking to other unit tests * Feature: WIP addition of hasOne directive * WIP Feature: V2 connection directive replacements hasOne, hasMany, belongsTo manyToMany is to come in a future branch * Feature: WIP addition of hasOne directive * Fix: Purge FeatureFlags cli core usage, update tests * Fix all the merge conflicts I already resolved but git re-broke * Feature: Add manyToMany directive and tests * Feature: Add manyToMany directive and tests remake a few changes that somehow got blown away * Fix: move a visitor test to a visitor test file * Fix: Reuse some variables instead of remaking the value * Fix: case on generated field names manyToMany
1 parent fe5d8ff commit cf95f9c

File tree

5 files changed

+342
-8
lines changed

5 files changed

+342
-8
lines changed

packages/appsync-modelgen-plugin/src/__tests__/utils/process-connections.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import {
22
processConnections,
33
CodeGenConnectionType,
4-
CodeGenFieldConnection,
54
CodeGenFieldConnectionHasMany,
65
CodeGenFieldConnectionBelongsTo,
76
CodeGenFieldConnectionHasOne,
87
getConnectedField,
98
} from '../../utils/process-connections';
10-
import { CodeGenModelMap, CodeGenModel } from '../../visitors/appsync-visitor';
9+
import { CodeGenModelMap } from '../../visitors/appsync-visitor';
1110
import { processConnectionsV2 } from '../../utils/process-connections-v2';
1211

1312
describe('process connection', () => {
@@ -772,5 +771,6 @@ describe('process connection', () => {
772771
expect(connectionInfo.isConnectingFieldAutoCreated).toEqual(false);
773772
});
774773
});
774+
775775
});
776776
});

packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-visitor.test.ts

Lines changed: 183 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
import { buildSchema, parse, visit } from 'graphql';
22
import { directives, scalars } from '../../scalars/supported-directives';
33
import { CodeGenConnectionType, CodeGenFieldConnectionBelongsTo, CodeGenFieldConnectionHasMany } from '../../utils/process-connections';
4-
import { AppSyncModelVisitor, CodeGenField, CodeGenGenerateEnum } from '../../visitors/appsync-visitor';
4+
import { AppSyncModelVisitor, CodeGenGenerateEnum } from '../../visitors/appsync-visitor';
55

66
const buildSchemaWithDirectives = (schema: String) => {
77
return buildSchema([schema, directives, scalars].join('\n'));
88
};
99

10-
const createAndGenerateVisitor = (schema: string) => {
10+
const createAndGenerateVisitor = (schema: string, usePipelinedTransformer: boolean = false) => {
1111
const ast = parse(schema);
1212
const builtSchema = buildSchemaWithDirectives(schema);
1313
const visitor = new AppSyncModelVisitor(
1414
builtSchema,
15-
{ directives, target: 'general', isTimestampFieldsAdded: true },
15+
{ directives, target: 'general', isTimestampFieldsAdded: true, usePipelinedTransformer: usePipelinedTransformer },
1616
{ generate: CodeGenGenerateEnum.code },
1717
);
1818
visit(ast, { leave: visitor });
1919
visitor.generate();
2020
return visitor;
2121
};
2222

23+
const createAndGeneratePipelinedTransformerVisitor = (
24+
schema: string
25+
) => {
26+
return createAndGenerateVisitor(schema, true);
27+
};
28+
2329
describe('AppSyncModelVisitor', () => {
2430

2531
it('should support schema with id', () => {
@@ -563,4 +569,178 @@ describe('AppSyncModelVisitor', () => {
563569
expect(updatedAtField).toMatchObject(updatedAtFieldObj);
564570
});
565571
});
572+
573+
describe('manyToMany testing', () => {
574+
let simpleManyToManySchema;
575+
let simpleManyModelMap;
576+
let transformedSimpleManyModelMap;
577+
578+
beforeEach(() => {
579+
simpleManyToManySchema = /* GraphQL */ `
580+
type Human @model {
581+
governmentID: ID! @primaryKey
582+
pets: [Animal] @manyToMany(relationName: "PetFriend")
583+
}
584+
585+
type Animal @model {
586+
animalTag: ID!
587+
humanFriend: [Human] @manyToMany(relationName: "PetFriend")
588+
}
589+
`;
590+
591+
simpleManyModelMap = {
592+
Human: {
593+
name: 'Human',
594+
type: 'model',
595+
directives: [],
596+
fields: [
597+
{
598+
type: 'ID',
599+
isNullable: false,
600+
isList: false,
601+
name: 'governmentID',
602+
directives: [{ name: 'primaryKey', arguments: {} }]
603+
},
604+
{
605+
type: 'Animal',
606+
isNullable: true,
607+
isList: true,
608+
name: 'pets',
609+
directives: [{ name: 'manyToMany', arguments: { relationName: 'PetFriend' } }]
610+
}
611+
]
612+
},
613+
Animal: {
614+
name: 'Animal',
615+
type: 'model',
616+
directives: [],
617+
fields: [
618+
{
619+
type: 'ID',
620+
isNullable: false,
621+
isList: false,
622+
name: 'animalTag',
623+
directives: [],
624+
},
625+
{
626+
type: 'Human',
627+
isNullable: true,
628+
isList: true,
629+
name: 'postID',
630+
directives: [{ name: 'manyToMany', arguments: { relationName: 'PetFriend' } }]
631+
}
632+
],
633+
}
634+
};
635+
});
636+
637+
transformedSimpleManyModelMap = {
638+
Human: {
639+
name: 'Human',
640+
type: 'model',
641+
directives: [{name: 'model', arguments: {}}],
642+
fields: [
643+
{
644+
type: 'ID',
645+
isNullable: false,
646+
isList: false,
647+
name: 'governmentID',
648+
directives: [{ name: 'primaryKey', arguments: {} }]
649+
},
650+
{
651+
type: 'PetFriend',
652+
isNullable: true,
653+
isList: true,
654+
name: 'pets',
655+
directives: [{ name: 'hasMany', arguments: { fields: ['governmentID'] } }]
656+
}
657+
]
658+
},
659+
Animal: {
660+
name: 'Animal',
661+
type: 'model',
662+
directives: [{name: 'model', arguments: {}}],
663+
fields: [
664+
{
665+
type: 'ID',
666+
isNullable: false,
667+
isList: false,
668+
name: 'animalTag',
669+
directives: [],
670+
},
671+
{
672+
type: 'PetFriend',
673+
isNullable: true,
674+
isList: true,
675+
name: 'postID',
676+
directives: [{ name: 'hasMany', arguments: { fields: ['id'] } }]
677+
}
678+
],
679+
},
680+
PetFriend: {
681+
name: 'PetFriend',
682+
type: 'model',
683+
directives: [{name: 'model', arguments: {}}],
684+
fields: [
685+
{
686+
type: 'ID',
687+
isNullable: false,
688+
isList: false,
689+
name: 'id',
690+
directives: []
691+
},
692+
{
693+
type: 'ID',
694+
isNullable: false,
695+
isList: false,
696+
name: 'humanID',
697+
directives: [{ name: 'index', arguments: { name: 'byHuman', sortKeyFields: ['animalID'] } }]
698+
},
699+
{
700+
type: 'ID',
701+
isNullable: false,
702+
isList: false,
703+
name: 'animalID',
704+
directives: [{ name: 'index', arguments: { name: 'byAnimal', sortKeyFields: ['humanID'] } }]
705+
},
706+
{
707+
type: 'Human',
708+
isNullable: false,
709+
isList: false,
710+
name: 'human',
711+
directives: [{ name: 'belongsTo', arguments: { fields: ['humanID'] } }]
712+
},
713+
{
714+
type: 'Animal',
715+
isNullable: false,
716+
isList: false,
717+
name: 'animal',
718+
directives: [{ name: 'belongsTo', arguments: { fields: ['humanID'] } }]
719+
}
720+
]
721+
}
722+
};
723+
724+
it('Should correctly convert the model map of a simple manyToMany', () => {
725+
const visitor = createAndGeneratePipelinedTransformerVisitor(simpleManyToManySchema);
726+
727+
expect(visitor.models.Human.fields.length).toEqual(5);
728+
expect(visitor.models.Human.fields[2].directives[0].name).toEqual('hasMany')
729+
expect(visitor.models.Human.fields[2].directives[0].arguments.fields.length).toEqual(1)
730+
expect(visitor.models.Human.fields[2].directives[0].arguments.fields[0]).toEqual('governmentID')
731+
expect(visitor.models.Human.fields[2].directives[0].arguments.indexName).toEqual('byHuman')
732+
expect(visitor.models.PetFriend).toBeDefined();
733+
expect(visitor.models.PetFriend.fields.length).toEqual(5);
734+
expect(visitor.models.PetFriend.fields[2].directives[0].name).toEqual('belongsTo')
735+
expect(visitor.models.PetFriend.fields[2].directives[0].arguments.fields.length).toEqual(1)
736+
expect(visitor.models.PetFriend.fields[2].directives[0].arguments.fields[0]).toEqual('animalID')
737+
expect(visitor.models.Animal.fields.length).toEqual(5);
738+
expect(visitor.models.Animal.fields[2].type).toEqual('PetFriend');
739+
expect(visitor.models.Animal.fields[2].directives.length).toEqual(1);
740+
expect(visitor.models.Animal.fields[2].directives[0].name).toEqual('hasMany');
741+
expect(visitor.models.Animal.fields[2].directives[0].arguments.fields.length).toEqual(1)
742+
expect(visitor.models.Animal.fields[2].directives[0].arguments.fields[0]).toEqual('id')
743+
expect(visitor.models.Animal.fields[2].directives[0].arguments.indexName).toEqual('byAnimal')
744+
});
745+
});
566746
});

packages/appsync-modelgen-plugin/src/utils/process-connections-v2.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CodeGenField, CodeGenFieldDirective, CodeGenModel, CodeGenModelMap } from '../visitors/appsync-visitor';
1+
import { CodeGenDirective, CodeGenField, CodeGenFieldDirective, CodeGenModel, CodeGenModelMap } from '../visitors/appsync-visitor';
22
import {
33
CodeGenFieldConnection, DEFAULT_HASH_KEY_FIELD, flattenFieldDirectives,
44
getDirective,
@@ -10,12 +10,41 @@ import { processHasManyConnection } from './process-has-many';
1010

1111
// TODO: This file holds several references to utility functions in the v1 process connections file, those functions need to go here before that file is removed
1212

13+
function getBelongsToConnectedField(field: CodeGenField, model: CodeGenModel, connectedModel: CodeGenModel, connectionInfo: CodeGenDirective): CodeGenField | undefined {
14+
if(connectionInfo.arguments.fields) {
15+
let indexDirective = flattenFieldDirectives(model).find(dir => {
16+
return dir.name === 'index' && dir.fieldName === connectionInfo.arguments.fields[0];
17+
});
18+
19+
if(indexDirective) {
20+
let theIndex = indexDirective;
21+
let otherSideConnected = flattenFieldDirectives(connectedModel).find(dir => {
22+
return (dir.name === 'hasOne' || dir.name === 'hasMany') && dir.arguments.indexName === theIndex.arguments.name;
23+
});
24+
if(otherSideConnected) {
25+
for(let connField of connectedModel.fields) {
26+
if (connField.name === otherSideConnected?.fieldName) {
27+
return connField;
28+
}
29+
}
30+
}
31+
}
32+
}
33+
}
34+
1335
export function getConnectedFieldV2(field: CodeGenField, model: CodeGenModel, connectedModel: CodeGenModel, directiveName: string): CodeGenField {
1436
const connectionInfo = getDirective(field)(directiveName);
1537
if (!connectionInfo) {
1638
throw new Error(`The ${field.name} on model ${model.name} is not connected`);
1739
}
1840

41+
if(connectionInfo.name === 'belongsTo') {
42+
let connectedFieldBelongsTo = getBelongsToConnectedField(field, model, connectedModel, connectionInfo);
43+
if (connectedFieldBelongsTo) {
44+
return connectedFieldBelongsTo;
45+
}
46+
}
47+
1948
const indexName = connectionInfo.arguments.indexName;
2049
const connectionFields = connectionInfo.arguments.fields;
2150
if (connectionFields) {

packages/appsync-modelgen-plugin/src/visitors/appsync-swift-visitor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export interface ParsedAppSyncModelSwiftConfig extends ParsedAppSyncModelConfig
4141
export class AppSyncSwiftVisitor<
4242
TRawConfig extends RawAppSyncModelSwiftConfig = RawAppSyncModelSwiftConfig,
4343
TPluginConfig extends ParsedAppSyncModelSwiftConfig = ParsedAppSyncModelSwiftConfig
44-
> extends AppSyncModelVisitor<TRawConfig, TPluginConfig> {
44+
> extends AppSyncModelVisitor<TRawConfig, TPluginConfig> {
4545
protected modelExtensionImports: string[] = ['import Amplify', 'import Foundation'];
4646
protected imports: string[] = ['import Amplify', 'import Foundation'];
4747

0 commit comments

Comments
 (0)