Skip to content

Commit

Permalink
Merge pull request #829 from aws-amplify/main
Browse files Browse the repository at this point in the history
Release references codegen for native
  • Loading branch information
dpilch authored Apr 29, 2024
2 parents d2f08ba + a909ff3 commit 3ae2d85
Show file tree
Hide file tree
Showing 13 changed files with 382 additions and 1,459 deletions.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ describe('custom references', () => {
}
`;

const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema);
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: true });
expect(visitor.generate()).toMatchSnapshot();
});

Expand All @@ -543,7 +543,7 @@ describe('custom references', () => {
}
`;

const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema);
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: true });
expect(visitor.generate()).toMatchSnapshot();
});

Expand All @@ -567,7 +567,7 @@ describe('custom references', () => {
primary: Primary @belongsTo(references: ["primaryId"])
}
`;
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema);
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: true });
expect(visitor.generate()).toMatchSnapshot();
});

Expand All @@ -588,7 +588,7 @@ describe('custom references', () => {
}
`;

const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema);
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: true });
expect(visitor.generate()).toMatchSnapshot();
});

Expand All @@ -611,7 +611,7 @@ describe('custom references', () => {
}
`;

const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema);
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: true });
expect(visitor.generate()).toMatchSnapshot();
});

Expand All @@ -633,7 +633,7 @@ describe('custom references', () => {
primary: Primary @belongsTo(references: ["primaryTenantId", "primaryInstanceId", "primaryRecordId"])
}
`;
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema);
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: true });
expect(visitor.generate()).toMatchSnapshot();
});

Expand All @@ -653,7 +653,7 @@ describe('custom references', () => {
}
`;

const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema);
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: true });
expect(() => visitor.generate())
.toThrowError(`'fields' and 'references' cannot be used together.`);
});
Expand All @@ -674,7 +674,7 @@ describe('custom references', () => {
}
`;

const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema);
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: true });
expect(() => visitor.generate())
.toThrowError(`'fields' and 'references' cannot be used together.`);
});
Expand All @@ -695,7 +695,7 @@ describe('custom references', () => {
}
`;

const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema);
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: true });
expect(() => visitor.generate())
.toThrowError(`'fields' and 'references' cannot be used together.`);
});
Expand All @@ -716,7 +716,7 @@ describe('custom references', () => {
}
`;

const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema);
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: true });
expect(() => visitor.generate())
.toThrowError(`Error processing @hasOne directive on SqlPrimary.related. @belongsTo directive with references ["primaryId"] was not found in connected model SqlRelated`);
});
Expand All @@ -737,11 +737,33 @@ describe('custom references', () => {
}
`;

const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema);
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: true });
expect(() => visitor.generate())
.toThrowError(`Error processing @belongsTo directive on SqlRelated.primary. @hasOne or @hasMany directive with references ["primaryId"] was not found in connected model SqlPrimary`);
});

test('throws error when missing references on hasMany related model when custom pk is disabled', () => {
const schema = /* GraphQL */ `
type SqlPrimary @refersTo(name: "sql_primary") @model {
id: Int! @primaryKey
content: String
related: [SqlRelated] @hasMany(references: ["primaryId"])
}
type SqlRelated @refersTo(name: "sql_related") @model {
id: Int! @primaryKey
content: String
primaryId: Int! @refersTo(name: "primary_id") @index(name: "primary_id")
primary: SqlPrimary @belongsTo
}
`;

const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: false });
expect(() => visitor.generate())
.toThrowError(`Error processing @hasMany directive on SqlPrimary.related. @belongsTo directive with references ["primaryId"] was not found in connected model SqlRelated`);
});


test('throws error when missing references on hasMany related model', () => {
const schema = /* GraphQL */ `
type SqlPrimary @refersTo(name: "sql_primary") @model {
Expand All @@ -758,7 +780,7 @@ describe('custom references', () => {
}
`;

const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema);
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: true });
expect(() => visitor.generate())
.toThrowError(`Error processing @hasMany directive on SqlPrimary.related. @belongsTo directive with references ["primaryId"] was not found in connected model SqlRelated`);
});
Expand All @@ -779,7 +801,7 @@ describe('custom references', () => {
}
`;

const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema);
const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema, { respectPrimaryKeyAttributesOnConnectionField: true });
expect(() => visitor.generate())
.toThrowError(`Error processing @belongsTo directive on SqlRelated.primary. @hasOne or @hasMany directive with references ["primaryId"] was not found in connected model SqlPrimary`);
});
Expand Down
20 changes: 10 additions & 10 deletions packages/appsync-modelgen-plugin/src/utils/process-belongs-to.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
flattenFieldDirectives,
makeConnectionAttributeName,
} from './process-connections';
import { getConnectedFieldV2, fieldsAndReferencesErrorMessage } from './process-connections-v2';
import { getConnectedFieldV2, getConnectedFieldForReferences } from './process-connections-v2';


export function processBelongsToConnection(
Expand All @@ -15,7 +15,6 @@ export function processBelongsToConnection(
modelMap: CodeGenModelMap,
connectionDirective: CodeGenDirective,
isCustomPKEnabled: boolean = false,
respectReferences: boolean = false, // remove when enabled references for all targets
): CodeGenFieldConnection | undefined {
if (field.isList) {
throw new Error(
Expand All @@ -29,22 +28,23 @@ export function processBelongsToConnection(
`A 'belongsTo' field should match to a corresponding 'hasMany' or 'hasOne' field`
);
}
const otherSideField = isCustomPKEnabled ? otherSideConnectedFields[0] : getConnectedFieldV2(field, model, otherSide, connectionDirective.name, false, respectReferences);
const connectionFields = connectionDirective.arguments.fields || [];

const references = connectionDirective.arguments.references || [];

if (connectionFields.length > 0 && references.length > 0) {
throw new Error(fieldsAndReferencesErrorMessage);
const isUsingReferences = references.length > 0;
if (isUsingReferences) {
// ensure there is a matching hasOne/hasMany field with references
getConnectedFieldForReferences(field, model, otherSide, connectionDirective.name)
}

const otherSideField = isCustomPKEnabled ? otherSideConnectedFields[0] : getConnectedFieldV2(field, model, otherSide, connectionDirective.name);
const connectionFields = connectionDirective.arguments.fields || [];

// if a type is connected using name, then amplify-graphql-relational-transformer adds a field to
// track the connection and that field is not part of the selection set
// but if the field are connected using fields argument in connection directive
// we are reusing the field and it should be preserved in selection set
const otherSideHasMany = otherSideField.isList;
const isUsingReferences = respectReferences && references.length > 0;
// New metada type introduced by custom PK v2 support
let targetNames = isUsingReferences ? [ ...connectionFields, ...references ] : [ ...connectionFields ];
let targetNames: string[] = [ ...connectionFields, ...references ];
if (targetNames.length === 0) {
if (otherSideHasMany) {
targetNames = isCustomPKEnabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,14 @@ export function getConnectedFieldV2(
model: CodeGenModel,
connectedModel: CodeGenModel,
directiveName: string,
shouldUseModelNameFieldInHasManyAndBelongsTo: boolean = false,
respectReferences: boolean = false,
shouldUseModelNameFieldInHasManyAndBelongsTo: boolean = false
): CodeGenField {
const connectionInfo = getDirective(field)(directiveName);
if (!connectionInfo) {
throw new Error(`The ${field.name} on model ${model.name} is not connected`);
}

const references = connectionInfo.arguments.references;
if (connectionInfo.name === 'belongsTo') {
let connectedFieldsBelongsTo = getBelongsToConnectedFields(model, connectedModel);
if (respectReferences && references) {
const connectedField = connectedFieldsBelongsTo.find((field) => {
return field.directives.some((dir) => {
return (dir.name === 'hasOne' || dir.name === 'hasMany')
&& dir.arguments.references
&& JSON.stringify(dir.arguments.references) === JSON.stringify(connectionInfo.arguments.references);
});
});
if (!connectedField) {
throw new Error(`Error processing @belongsTo directive on ${model.name}.${field.name}. @hasOne or @hasMany directive with references ${JSON.stringify(connectionInfo.arguments?.references)} was not found in connected model ${connectedModel.name}`);
}
return connectedField;
}

if (connectedFieldsBelongsTo.length === 1) {
return connectedFieldsBelongsTo[0];
Expand All @@ -45,25 +29,10 @@ export function getConnectedFieldV2(

const indexName = connectionInfo.arguments.indexName;
const connectionFields = connectionInfo.arguments.fields;
if (connectionFields && references) {
throw new Error(fieldsAndReferencesErrorMessage);
}
if (references || connectionFields || directiveName === 'hasOne') {
if (connectionFields || directiveName === 'hasOne') {
let connectionDirective;
if (respectReferences && references) {
if (connectionInfo) {
connectionDirective = flattenFieldDirectives(connectedModel).find((dir) => {
return dir.arguments.references
&& JSON.stringify(dir.arguments.references) === JSON.stringify(connectionInfo.arguments.references);
});
if (!connectionDirective) {
throw new Error(`Error processing @${connectionInfo.name} directive on ${model.name}.${field.name}. @belongsTo directive with references ${JSON.stringify(connectionInfo.arguments?.references)} was not found in connected model ${connectedModel.name}`);
}
}
}

// Find gsi on other side if index is defined
else if (indexName) {
if (indexName) {
connectionDirective = flattenFieldDirectives(connectedModel).find(dir => {
return dir.name === 'index' && dir.arguments.name === indexName;
});
Expand Down Expand Up @@ -155,29 +124,76 @@ export function getConnectedFieldV2(
};
}

export function getConnectedFieldForReferences(
field: CodeGenField,
model: CodeGenModel,
connectedModel: CodeGenModel,
directiveName: string,
): CodeGenField {
const connectionInfo = getDirective(field)(directiveName);
if (!connectionInfo) {
throw new Error(`The ${field.name} on model ${model.name} is not connected`);
}
const references = connectionInfo.arguments.references;
if (!references) {
throw new Error(`The ${field.name} on model ${model.name} does not have references.`);
}
const connectionFields = connectionInfo.arguments.fields;
if (connectionFields && references) {
throw new Error(`'fields' and 'references' cannot be used together.`);
}

if (connectionInfo.name === 'belongsTo') {
let connectedFieldsBelongsTo = getBelongsToConnectedFields(model, connectedModel);
const connectedField = connectedFieldsBelongsTo.find((field) => {
return field.directives.some((dir) => {
return (dir.name === 'hasOne' || dir.name === 'hasMany')
&& dir.arguments.references
&& JSON.stringify(dir.arguments.references) === JSON.stringify(connectionInfo.arguments.references);
});
});
if (!connectedField) {
throw new Error(`Error processing @belongsTo directive on ${model.name}.${field.name}. @hasOne or @hasMany directive with references ${JSON.stringify(connectionInfo.arguments?.references)} was not found in connected model ${connectedModel.name}`);
}
return connectedField;
}

// hasOne and hasMany
const connectionDirective = flattenFieldDirectives(connectedModel).find((dir) => {
return dir.arguments.references
&& JSON.stringify(dir.arguments.references) === JSON.stringify(connectionInfo.arguments.references);
});
if (!connectionDirective) {
throw new Error(`Error processing @${connectionInfo.name} directive on ${model.name}.${field.name}. @belongsTo directive with references ${JSON.stringify(connectionInfo.arguments?.references)} was not found in connected model ${connectedModel.name}`);
}
const connectedFieldName = ((fieldDir: CodeGenFieldDirective) => {
return fieldDir.fieldName;
})(connectionDirective as CodeGenFieldDirective)

const connectedField = connectedModel.fields.find(f => f.name === connectedFieldName);
return connectedField!;
}

export function processConnectionsV2(
field: CodeGenField,
model: CodeGenModel,
modelMap: CodeGenModelMap,
shouldUseModelNameFieldInHasManyAndBelongsTo: boolean = false,
isCustomPKEnabled: boolean = false,
shouldUseFieldsInAssociatedWithInHasOne: boolean = false,
respectReferences: boolean = false, // remove when enabled references for all targets
): CodeGenFieldConnection | undefined {
const connectionDirective = field.directives.find(d => d.name === 'hasOne' || d.name === 'hasMany' || d.name === 'belongsTo');

if (connectionDirective) {
switch (connectionDirective.name) {
case 'hasOne':
return processHasOneConnection(field, model, modelMap, connectionDirective, isCustomPKEnabled, shouldUseFieldsInAssociatedWithInHasOne, respectReferences);
return processHasOneConnection(field, model, modelMap, connectionDirective, isCustomPKEnabled, shouldUseFieldsInAssociatedWithInHasOne);
case 'belongsTo':
return processBelongsToConnection(field, model, modelMap, connectionDirective, isCustomPKEnabled, respectReferences);
return processBelongsToConnection(field, model, modelMap, connectionDirective, isCustomPKEnabled);
case 'hasMany':
return processHasManyConnection(field, model, modelMap, connectionDirective, shouldUseModelNameFieldInHasManyAndBelongsTo, isCustomPKEnabled, respectReferences);
return processHasManyConnection(field, model, modelMap, connectionDirective, shouldUseModelNameFieldInHasManyAndBelongsTo, isCustomPKEnabled);
default:
break;
}
}
}

export const fieldsAndReferencesErrorMessage = `'fields' and 'references' cannot be used together.`;
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type CodeGenFieldConnectionBelongsTo = CodeGenConnectionTypeBase & {
export type CodeGenFieldConnectionHasOne = CodeGenConnectionTypeBase & {
kind: CodeGenConnectionType.HAS_ONE;
associatedWith: CodeGenField;// Legacy field remained for backward compatability
associatedWithNativeReferences?: CodeGenField; // native uses the connected field instead of associatedWithFields
associatedWithFields: CodeGenField[]; // New attribute for v2 custom pk support
targetName?: string; // Legacy field remained for backward compatability
targetNames?: string[]; // New attribute for v2 custom pk support
Expand All @@ -30,6 +31,7 @@ export type CodeGenFieldConnectionHasOne = CodeGenConnectionTypeBase & {
export type CodeGenFieldConnectionHasMany = CodeGenConnectionTypeBase & {
kind: CodeGenConnectionType.HAS_MANY;
associatedWith: CodeGenField;// Legacy field remained for backward compatability
associatedWithNativeReferences?: CodeGenField; // native uses the connected field instead of associatedWithFields
associatedWithFields: CodeGenField[]; // New attribute for v2 custom pk support
};

Expand Down
Loading

0 comments on commit 3ae2d85

Please sign in to comment.