Skip to content

Commit ad053dc

Browse files
authored
fix(datastore): skip has-many model to graphQL translation (#2661)
* fix(datastore): skip has-many model to graphQL translation * address PR comments
1 parent 79d7a7c commit ad053dc

17 files changed

+422
-55
lines changed

AmplifyPlugins/Core/AWSPluginsCore/Model/Support/Model+GraphQL.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ extension Model {
5959
input.updateValue(nil, forKey: fieldName)
6060
}
6161
}
62+
} else if case .collection = modelField.type { // skip all "has-many"
63+
continue
6264
} else {
6365
input.updateValue(nil, forKey: name)
6466
}

AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/Statement+Model.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ extension Statement: StatementModelConvertible {
122122
// For example, when the value is the `id` of the Blog, then the field.isPrimaryKey is satisfied.
123123
// Every association of the Blog, such as the has-many Post is populated with the List with
124124
// associatedId == blog's id. This way, the list of post can be lazily loaded later using the associated id.
125-
if let id = modelValue as? String, field.isPrimaryKey {
125+
if let id = modelValue as? String,
126+
(field.name == ModelIdentifierFormat.Custom.sqlColumnName || // this is the `@@primaryKey` (CPK)
127+
(schema.primaryKey.fields.count == 1 // or there's only one primary key (not composite key)
128+
&& schema.primaryKey.indexOfField(named: field.name) != nil)) { // and this field is the primary key
126129
let associations = schema.fields.values.filter {
127130
$0.isArray && $0.hasAssociation
128131
}

AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginCPKTests/Models/primarykey_schema.graphql

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,18 @@ type Comment8 @model {
224224
# title: String!
225225
# description: String
226226
#}
227+
228+
## iOS 10. bi-directional has-many PostComment4V2
229+
230+
type Post4V2 @model @auth(rules: [{allow: public}]) {
231+
id: ID!
232+
title: String!
233+
comments: [Comment4V2] @hasMany(indexName: "byPost4", fields: ["id"])
234+
}
235+
236+
type Comment4V2 @model @auth(rules: [{allow: public}]) {
237+
id: ID!
238+
postID: ID! @index(name: "byPost4", sortKeyFields: ["content"])
239+
content: String!
240+
post: Post4V2 @belongsTo(fields: ["postID"])
241+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import XCTest
10+
import Amplify
11+
12+
/*
13+
## iOS 10. bi-directional has-many PostComment4V2
14+
15+
type Post4V2 @model @auth(rules: [{allow: public}]) {
16+
id: ID!
17+
title: String!
18+
comments: [Comment4V2] @hasMany(indexName: "byPost4", fields: ["id"])
19+
}
20+
21+
type Comment4V2 @model @auth(rules: [{allow: public}]) {
22+
id: ID!
23+
postID: ID! @index(name: "byPost4", sortKeyFields: ["content"])
24+
content: String!
25+
post: Post4V2 @belongsTo(fields: ["postID"])
26+
}
27+
28+
*/
29+
final class AWSDataStorePrimaryKeyPostComment4V2Test: AWSDataStorePrimaryKeyBaseTest {
30+
31+
/// Save Post4V2 and Comment4V2 and ensure they are synced successfully.
32+
/// This test is from the issue https://github.com/aws-amplify/amplify-swift/issues/2644
33+
/// The issue appears when using Amplify CLI v10.5.0 which defaults to CPK enabled for new projects.
34+
/// The swift models generated for this schema doesn't leverage CPK use case, however we are
35+
/// adding it here to ensure the CPK feature is backwards compatible for models with default identifiers.
36+
///
37+
/// - Given: Post and Comment instances
38+
/// - When:
39+
/// - DataStore.save
40+
/// - Then:
41+
/// - Data saved to the local store and synced to the cloud successfully.
42+
///
43+
func testSavePostComment4V2() async throws {
44+
setup(withModels: PostComment4V2())
45+
try await assertDataStoreReady()
46+
try await assertQuerySuccess(modelType: Post4V2.self)
47+
48+
let parent = Post4V2(title: "title")
49+
let child = Comment4V2(content: "content", post: parent)
50+
51+
// Mutations
52+
try await assertMutationsParentChild(parent: parent, child: child)
53+
}
54+
55+
/// Lazy Load the Comments from the Post object.
56+
/// This test is from the issue https://github.com/aws-amplify/amplify-swift/issues/2644
57+
/// The issue appears when using Amplify CLI v10.5.0 which defaults to CPK enabled for new projects.
58+
/// The swift models generated for this schema doesn't leverage CPK use case, however we are
59+
/// adding it here to ensure the CPK feature is backwards compatible for models with default identifiers.
60+
///
61+
/// - Given: Post with a Comment
62+
/// - When:
63+
/// - DataStore.query Post and traverse to the Comments
64+
/// - Then:
65+
/// - Comments are lazy loaded.
66+
///
67+
func testSavePostAndLazyLoadComments() async throws {
68+
setup(withModels: PostComment4V2())
69+
try await assertDataStoreReady()
70+
71+
let parent = Post4V2(title: "title")
72+
let child = Comment4V2(content: "content", post: parent)
73+
74+
// Mutations
75+
try await assertMutationsParentChild(parent: parent, child: child, shouldDeleteParent: false)
76+
77+
guard let queriedPost = try await Amplify.DataStore.query(Post4V2.self, byId: parent.id) else {
78+
XCTFail("Failed to query post")
79+
return
80+
}
81+
guard let comments = queriedPost.comments else{
82+
XCTFail("Lazy List of Comment should exist on post")
83+
return
84+
}
85+
try await comments.fetch()
86+
XCTAssertEqual(comments.count, 1)
87+
88+
try await assertDeleteMutation(parent: parent, child: child)
89+
}
90+
}
91+
92+
extension AWSDataStorePrimaryKeyPostComment4V2Test {
93+
struct PostComment4V2: AmplifyModelRegistration {
94+
public let version: String = "version"
95+
func registerModels(registry: ModelRegistry.Type) {
96+
ModelRegistry.register(modelType: Post4V2.self)
97+
ModelRegistry.register(modelType: Comment4V2.self)
98+
}
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// swiftlint:disable all
2+
import Amplify
3+
import Foundation
4+
5+
extension Comment4V2 {
6+
// MARK: - CodingKeys
7+
public enum CodingKeys: String, ModelKey {
8+
case id
9+
case content
10+
case post
11+
case createdAt
12+
case updatedAt
13+
}
14+
15+
public static let keys = CodingKeys.self
16+
// MARK: - ModelSchema
17+
18+
public static let schema = defineSchema { model in
19+
let comment4V2 = Comment4V2.keys
20+
21+
model.authRules = [
22+
rule(allow: .public, operations: [.create, .update, .delete, .read])
23+
]
24+
25+
model.pluralName = "Comment4V2s"
26+
27+
model.attributes(
28+
.index(fields: ["postID", "content"], name: "byPost4"),
29+
.primaryKey(fields: [comment4V2.id])
30+
)
31+
32+
model.fields(
33+
.field(comment4V2.id, is: .required, ofType: .string),
34+
.field(comment4V2.content, is: .required, ofType: .string),
35+
.belongsTo(comment4V2.post, is: .optional, ofType: Post4V2.self, targetNames: ["postID"]),
36+
.field(comment4V2.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime),
37+
.field(comment4V2.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime)
38+
)
39+
}
40+
}
41+
42+
extension Comment4V2: ModelIdentifiable {
43+
public typealias IdentifierFormat = ModelIdentifierFormat.Default
44+
public typealias IdentifierProtocol = DefaultModelIdentifier<Self>
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// swiftlint:disable all
2+
import Amplify
3+
import Foundation
4+
5+
public struct Comment4V2: Model {
6+
public let id: String
7+
public var content: String
8+
public var post: Post4V2?
9+
public var createdAt: Temporal.DateTime?
10+
public var updatedAt: Temporal.DateTime?
11+
12+
public init(id: String = UUID().uuidString,
13+
content: String,
14+
post: Post4V2? = nil) {
15+
self.init(id: id,
16+
content: content,
17+
post: post,
18+
createdAt: nil,
19+
updatedAt: nil)
20+
}
21+
internal init(id: String = UUID().uuidString,
22+
content: String,
23+
post: Post4V2? = nil,
24+
createdAt: Temporal.DateTime? = nil,
25+
updatedAt: Temporal.DateTime? = nil) {
26+
self.id = id
27+
self.content = content
28+
self.post = post
29+
self.createdAt = createdAt
30+
self.updatedAt = updatedAt
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// swiftlint:disable all
2+
import Amplify
3+
import Foundation
4+
5+
extension Post4V2 {
6+
// MARK: - CodingKeys
7+
public enum CodingKeys: String, ModelKey {
8+
case id
9+
case title
10+
case comments
11+
case createdAt
12+
case updatedAt
13+
}
14+
15+
public static let keys = CodingKeys.self
16+
// MARK: - ModelSchema
17+
18+
public static let schema = defineSchema { model in
19+
let post4V2 = Post4V2.keys
20+
21+
model.authRules = [
22+
rule(allow: .public, operations: [.create, .update, .delete, .read])
23+
]
24+
25+
model.pluralName = "Post4V2s"
26+
27+
model.attributes(
28+
.primaryKey(fields: [post4V2.id])
29+
)
30+
31+
model.fields(
32+
.field(post4V2.id, is: .required, ofType: .string),
33+
.field(post4V2.title, is: .required, ofType: .string),
34+
.hasMany(post4V2.comments, is: .optional, ofType: Comment4V2.self, associatedWith: Comment4V2.keys.post),
35+
.field(post4V2.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime),
36+
.field(post4V2.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime)
37+
)
38+
}
39+
}
40+
41+
extension Post4V2: ModelIdentifiable {
42+
public typealias IdentifierFormat = ModelIdentifierFormat.Default
43+
public typealias IdentifierProtocol = DefaultModelIdentifier<Self>
44+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// swiftlint:disable all
2+
import Amplify
3+
import Foundation
4+
5+
public struct Post4V2: Model {
6+
public let id: String
7+
public var title: String
8+
public var comments: List<Comment4V2>?
9+
public var createdAt: Temporal.DateTime?
10+
public var updatedAt: Temporal.DateTime?
11+
12+
public init(id: String = UUID().uuidString,
13+
title: String,
14+
comments: List<Comment4V2>? = []) {
15+
self.init(id: id,
16+
title: title,
17+
comments: comments,
18+
createdAt: nil,
19+
updatedAt: nil)
20+
}
21+
internal init(id: String = UUID().uuidString,
22+
title: String,
23+
comments: List<Comment4V2>? = [],
24+
createdAt: Temporal.DateTime? = nil,
25+
updatedAt: Temporal.DateTime? = nil) {
26+
self.id = id
27+
self.title = title
28+
self.comments = comments
29+
self.createdAt = createdAt
30+
self.updatedAt = updatedAt
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
9+
import Foundation
10+
import XCTest
11+
import Amplify
12+
13+
/*
14+
# iOS.7. A Has-Many/Belongs-To relationship, each with a composite key
15+
# Post with `id` and `title`, Comment with `id` and `content`
16+
17+
type PostWithCompositeKey @model {
18+
id: ID! @primaryKey(sortKeyFields: ["title"])
19+
title: String!
20+
comments: [CommentWithCompositeKey] @hasMany
21+
}
22+
23+
type CommentWithCompositeKey @model {
24+
id: ID! @primaryKey(sortKeyFields: ["content"])
25+
content: String!
26+
post: PostWithCompositeKey @belongsTo
27+
}
28+
29+
*/
30+
final class AWSDataStorePrimaryKeyPostCommentCompositeKeyTest: AWSDataStorePrimaryKeyBaseTest {
31+
32+
func testModelWithCompositePrimaryKeyAndAssociations() async throws {
33+
setup(withModels: CompositeKeyWithAssociations())
34+
try await assertDataStoreReady()
35+
try await assertQuerySuccess(modelType: PostWithCompositeKey.self)
36+
let parent = PostWithCompositeKey(title: "Post22")
37+
let child = CommentWithCompositeKey(content: "Comment", post: parent)
38+
39+
// Mutations
40+
try await assertMutationsParentChild(parent: parent, child: child)
41+
42+
// Child should not exists as we've deleted the parent
43+
try await assertModelDeleted(modelType: CommentWithCompositeKey.self,
44+
identifier: .identifier(id: child.id, content: child.content))
45+
}
46+
47+
/// - Given: a set models with a belongs-to association and composite primary keys
48+
/// - When:
49+
/// - the parent model is saved
50+
/// - a child model is saved
51+
/// - query the children by the parent identifier
52+
/// - Then:
53+
/// - query returns the saved child model
54+
func testModelWithCompositePrimaryKeyAndQueryPredicate() async throws {
55+
setup(withModels: CompositeKeyWithAssociations())
56+
57+
try await assertDataStoreReady()
58+
59+
let post = PostWithCompositeKey(title: "title")
60+
let comment = CommentWithCompositeKey(content: "content", post: post)
61+
_ = try await Amplify.DataStore.save(post)
62+
_ = try await Amplify.DataStore.save(comment)
63+
64+
let predicate = CommentWithCompositeKey.keys.post == post.identifier
65+
let savedComments = try await Amplify.DataStore.query(CommentWithCompositeKey.self, where: predicate)
66+
XCTAssertNotNil(savedComments)
67+
XCTAssertEqual(savedComments.count, 1)
68+
XCTAssertEqual(savedComments[0].id, comment.id)
69+
XCTAssertEqual(savedComments[0].content, comment.content)
70+
}
71+
72+
}
73+
extension AWSDataStorePrimaryKeyPostCommentCompositeKeyTest {
74+
struct CompositeKeyWithAssociations: AmplifyModelRegistration {
75+
public let version: String = "version"
76+
func registerModels(registry: ModelRegistry.Type) {
77+
ModelRegistry.register(modelType: PostWithCompositeKey.self)
78+
ModelRegistry.register(modelType: CommentWithCompositeKey.self)
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)