Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release references codegen for native #829

Merged
merged 2 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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
Loading