From e886cf011b633a46212b8bd4f9f026668763acce Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Tue, 16 Jul 2024 09:07:24 -0400 Subject: [PATCH] fix(data): Gen2 hasOne associatedFields compile issue (#3700) --- .../Schema/ModelField+Association.swift | 16 +++++++++++----- .../Schema/ModelSchema+Definition.swift | 15 +++++++++++++++ .../AWSAPIPlugin/Core/AppSyncListProvider.swift | 2 +- .../Gen2_4/Customer4+Schema.swift | 2 +- .../Model/Support/Model+GraphQL.swift | 2 +- .../Model/Support/QueryPredicate+GraphQL.swift | 2 +- .../Storage/SQLite/ModelSchema+SQLite.swift | 2 +- .../Storage/SQLite/Statement+Model.swift | 2 +- .../DataStore/ModelFieldAssociationTests.swift | 17 +++++++++++++++-- 9 files changed, 47 insertions(+), 13 deletions(-) diff --git a/Amplify/Categories/DataStore/Model/Internal/Schema/ModelField+Association.swift b/Amplify/Categories/DataStore/Model/Internal/Schema/ModelField+Association.swift index 6dfafe2c1b..4ebe4bcb5a 100644 --- a/Amplify/Categories/DataStore/Model/Internal/Schema/ModelField+Association.swift +++ b/Amplify/Categories/DataStore/Model/Internal/Schema/ModelField+Association.swift @@ -88,7 +88,7 @@ import Foundation /// directly by host applications. The behavior of this may change without warning. public enum ModelAssociation { case hasMany(associatedFieldName: String?, associatedFieldNames: [String] = []) - case hasOne(associatedFieldName: String?, targetNames: [String]) + case hasOne(associatedFieldName: String?, associatedFieldNames: [String] = [], targetNames: [String]) case belongsTo(associatedFieldName: String?, targetNames: [String]) public static let belongsTo: ModelAssociation = .belongsTo(associatedFieldName: nil, targetNames: []) @@ -108,14 +108,20 @@ public enum ModelAssociation { ) } - @available(*, deprecated, message: "Use hasOne(associatedWith:targetNames:)") + @available(*, deprecated, message: "Use hasOne(associatedWith:associatedFields:targetNames:)") public static func hasOne(associatedWith: CodingKey?, targetName: String? = nil) -> ModelAssociation { let targetNames = targetName.map { [$0] } ?? [] return .hasOne(associatedWith: associatedWith, targetNames: targetNames) } - public static func hasOne(associatedWith: CodingKey?, targetNames: [String] = []) -> ModelAssociation { - return .hasOne(associatedFieldName: associatedWith?.stringValue, targetNames: targetNames) + public static func hasOne( + associatedWith: CodingKey? = nil, + associatedFields: [CodingKey] = [], + targetNames: [String] = []) -> ModelAssociation { + return .hasOne( + associatedFieldName: associatedWith?.stringValue, + associatedFieldNames: associatedFields.map { $0.stringValue }, + targetNames: targetNames) } @available(*, deprecated, message: "Use belongsTo(associatedWith:targetNames:)") @@ -254,7 +260,7 @@ extension ModelField { let associatedModel = requiredAssociatedModelName switch association { case .belongsTo(let associatedKey, _), - .hasOne(let associatedKey, _), + .hasOne(let associatedKey, _, _), .hasMany(let associatedKey, _): // TODO handle modelName casing (convert to camelCase) let key = associatedKey ?? associatedModel diff --git a/Amplify/Categories/DataStore/Model/Internal/Schema/ModelSchema+Definition.swift b/Amplify/Categories/DataStore/Model/Internal/Schema/ModelSchema+Definition.swift index b27d40bb55..35e6f1210d 100644 --- a/Amplify/Categories/DataStore/Model/Internal/Schema/ModelSchema+Definition.swift +++ b/Amplify/Categories/DataStore/Model/Internal/Schema/ModelSchema+Definition.swift @@ -279,6 +279,21 @@ public enum ModelFieldDefinition { association: .hasOne(associatedWith: associatedKey, targetNames: targetNames)) } + public static func hasOne(_ key: CodingKey, + is nullability: ModelFieldNullability = .required, + isReadOnly: Bool = false, + ofType type: Model.Type, + associatedFields associatedKeys: [CodingKey], + targetNames: [String] = []) -> ModelFieldDefinition { + return .field(key, + is: nullability, + isReadOnly: isReadOnly, + ofType: .model(type: type), + association: .hasOne(associatedWith: associatedKeys.first, + associatedFields: associatedKeys, + targetNames: targetNames)) + } + public static func belongsTo(_ key: CodingKey, is nullability: ModelFieldNullability = .required, isReadOnly: Bool = false, diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Core/AppSyncListProvider.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Core/AppSyncListProvider.swift index 4f1eaab6f0..19f1368dd6 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Core/AppSyncListProvider.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Core/AppSyncListProvider.swift @@ -268,7 +268,7 @@ public class AppSyncListProvider: ModelListProvider { } let defaultFieldName = modelSchema.name.camelCased() + field.pascalCased() + "Id" switch modelField.association { - case .belongsTo(_, let targetNames), .hasOne(_, let targetNames): + case .belongsTo(_, let targetNames), .hasOne(_, _, let targetNames): guard !targetNames.isEmpty else { return [defaultFieldName] diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/Gen2_4/Customer4+Schema.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/Gen2_4/Customer4+Schema.swift index b153fe844f..56b085c0c3 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/Gen2_4/Customer4+Schema.swift +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/Gen2_4/Customer4+Schema.swift @@ -32,7 +32,7 @@ extension Customer4 { model.fields( .field(customer4.id, is: .required, ofType: .string), .field(customer4.name, is: .optional, ofType: .string), - .hasOne(customer4.activeCart, is: .optional, ofType: Cart4.self, associatedWith: Cart4.keys.customer), + .hasOne(customer4.activeCart, is: .optional, ofType: Cart4.self, associatedFields: [Cart4.keys.customer]), .field(customer4.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime), .field(customer4.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime) ) diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/Model+GraphQL.swift b/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/Model+GraphQL.swift index 2e4c075b2c..7b78a2dc47 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/Model+GraphQL.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/Model+GraphQL.swift @@ -229,7 +229,7 @@ extension Model { let defaultFieldName = modelName.camelCased() + modelField.name.pascalCased() + "Id" if case let .belongsTo(_, targetNames) = modelField.association, !targetNames.isEmpty { return targetNames - } else if case let .hasOne(_, targetNames) = modelField.association, + } else if case let .hasOne(_, _, targetNames) = modelField.association, !targetNames.isEmpty { return targetNames } diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/QueryPredicate+GraphQL.swift b/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/QueryPredicate+GraphQL.swift index a8aca2529e..f2e3a6f816 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/QueryPredicate+GraphQL.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/QueryPredicate+GraphQL.swift @@ -131,7 +131,7 @@ extension QueryPredicateOperation: GraphQLFilterConvertible { } let targetName = targetNames.first ?? defaultFieldName return targetName - case .hasOne(_, let targetNames): + case .hasOne(_, _, let targetNames): guard targetNames.count == 1 else { preconditionFailure("QueryPredicate not supported on associated field with composite key: \(field)") } diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/ModelSchema+SQLite.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/ModelSchema+SQLite.swift index 2cee4cccc6..ab3bf52aff 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/ModelSchema+SQLite.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/ModelSchema+SQLite.swift @@ -82,7 +82,7 @@ extension ModelField: SQLColumn { var sqlName: String { if case let .belongsTo(_, targetNames) = association { return foreignKeySqlName(withAssociationTargets: targetNames) - } else if case let .hasOne(_, targetNames) = association { + } else if case let .hasOne(_, _, targetNames) = association { return foreignKeySqlName(withAssociationTargets: targetNames) } return name diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/Statement+Model.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/Statement+Model.swift index 6045206d6d..4345db1a74 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/Statement+Model.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/Statement+Model.swift @@ -203,7 +203,7 @@ extension Statement: StatementModelConvertible { private func getTargetNames(field: ModelField) -> [String] { switch field.association { - case let .some(.hasOne(_, targetNames)): + case let .some(.hasOne(_, _, targetNames)): return targetNames case let .some(.belongsTo(_, targetNames)): return targetNames diff --git a/AmplifyTests/CategoryTests/DataStore/ModelFieldAssociationTests.swift b/AmplifyTests/CategoryTests/DataStore/ModelFieldAssociationTests.swift index 35eb70b5b1..fab9543e07 100644 --- a/AmplifyTests/CategoryTests/DataStore/ModelFieldAssociationTests.swift +++ b/AmplifyTests/CategoryTests/DataStore/ModelFieldAssociationTests.swift @@ -37,24 +37,37 @@ class ModelFieldAssociationTests: XCTestCase { func testHasOneWithCodingKeys() { let hasOne = ModelAssociation.hasOne(associatedWith: Comment.keys.post, targetNames: []) - guard case .hasOne(let fieldName, let target) = hasOne else { + guard case .hasOne(let fieldName, let fieldNames, let target) = hasOne else { XCTFail("Should create hasOne association") return } XCTAssertEqual(fieldName, Comment.keys.post.stringValue) + XCTAssertEqual(fieldNames, []) XCTAssertEqual(target, []) } func testHasOneWithCodingKeysWithTargetName() { let hasOne = ModelAssociation.hasOne(associatedWith: Comment.keys.post, targetNames: ["postID"]) - guard case .hasOne(let fieldName, let target) = hasOne else { + guard case .hasOne(let fieldName, let fieldNames, let target) = hasOne else { XCTFail("Should create hasOne association") return } XCTAssertEqual(fieldName, Comment.keys.post.stringValue) + XCTAssertEqual(fieldNames, []) XCTAssertEqual(target, ["postID"]) } + func testHasOneWithCodingKeysWithAssociatedFields() { + let hasOne = ModelAssociation.hasOne(associatedFields: [Comment.keys.post]) + guard case .hasOne(let fieldName, let fieldNames, let target) = hasOne else { + XCTFail("Should create hasOne association") + return + } + XCTAssertEqual(fieldName, nil) + XCTAssertEqual(fieldNames, ["post"]) + XCTAssertEqual(target, []) + } + func testBelongsToWithTargetName() { let belongsTo = ModelAssociation.belongsTo(targetName: "postID") guard case .belongsTo(let fieldName, let target) = belongsTo else {