Skip to content

Commit 7e859db

Browse files
author
Di Wu
authored
fix(datastore): support CPK with sortKey for GraphQL scalar types (#3012)
* fix(datastore): support CPK with sortKey in GraphQL scalar type * fix typo * remove redundant type extension
1 parent a01dfe5 commit 7e859db

File tree

20 files changed

+842
-18
lines changed

20 files changed

+842
-18
lines changed

.github/workflows/integ_test_datastore_cpk.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ on:
55

66
permissions:
77
id-token: write
8-
contents: read
8+
contents: read
99

1010
jobs:
1111
datastore-integration-cpk-test:
1212
timeout-minutes: 30
13-
runs-on: macos-12
13+
runs-on: macos-13
1414
environment: IntegrationTest
1515
steps:
1616
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
@@ -33,5 +33,6 @@ jobs:
3333
with:
3434
project_path: ./AmplifyPlugins/DataStore/Tests/DataStoreHostApp
3535
scheme: AWSDataStorePluginCPKTests
36+
destination: 'platform=iOS Simulator,name=iPhone 14,OS=latest'
37+
xcode_path: '/Applications/Xcode_14.3.app'
3638

37-

AmplifyPlugins/Core/AWSPluginsCore/Model/Decorator/ModelIdDecorator.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Amplify
1111
/// Decorate the GraphQLDocument with the value of `ModelIdentifier` for a "delete" mutation or "get" query.
1212
public struct ModelIdDecorator: ModelBasedGraphQLDocumentDecorator {
1313
/// Array of model fields and their stringified value
14-
private var identifierFields: [(name: String, value: String, type: String)] = [(name: String, value: String, type: String)]()
14+
private var identifierFields = [(name: String, value: GraphQLDocumentValueRepresentable, type: String)]()
1515

1616
public init(model: Model, schema: ModelSchema) {
1717

@@ -33,7 +33,7 @@ public struct ModelIdDecorator: ModelBasedGraphQLDocumentDecorator {
3333
public init(identifierFields: [(name: String, value: Persistable)]) {
3434
var firstField = true
3535
identifierFields.forEach { name, value in
36-
self.identifierFields.append((name: name, value: "\(value)", type: firstField == true ? "ID!" : "String!"))
36+
self.identifierFields.append((name: name, value: Self.convert(persistable: value), type: firstField == true ? "ID!" : "String!"))
3737
firstField = false
3838
}
3939
}
@@ -76,17 +76,35 @@ public struct ModelIdDecorator: ModelBasedGraphQLDocumentDecorator {
7676
if case .mutation = document.operationType {
7777
var inputMap = [String: String]()
7878
for (name, value, _) in identifierFields {
79-
inputMap[name] = value
79+
inputMap[name] = value.graphQLDocumentValue
8080
}
8181
inputs["input"] = GraphQLDocumentInput(type: "\(document.name.pascalCased())Input!",
8282
value: .object(inputMap))
8383

8484
} else if case .query = document.operationType {
8585
for (name, value, type) in identifierFields {
86-
inputs[name] = GraphQLDocumentInput(type: type, value: .scalar(value))
86+
inputs[name] = GraphQLDocumentInput(
87+
type: type,
88+
value: identifierFields.count > 1 ? .inline(value) : .scalar(value)
89+
)
8790
}
8891
}
8992

9093
return document.copy(inputs: inputs)
9194
}
9295
}
96+
97+
fileprivate extension ModelIdDecorator {
98+
private static func convert(persistable: Persistable) -> GraphQLDocumentValueRepresentable {
99+
switch persistable {
100+
case let data as Double:
101+
return data
102+
case let data as Int:
103+
return data
104+
case let data as Bool:
105+
return data
106+
default:
107+
return "\(persistable)"
108+
}
109+
}
110+
}

AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLDocument/SingleDirectiveGraphQLDocument.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ extension SingleDirectiveGraphQLDocument {
7474
variables.updateValue(values, forKey: input.key)
7575
case .scalar(let value):
7676
variables.updateValue(value, forKey: input.key)
77+
case .inline:
78+
break
7779
}
7880

7981
}
@@ -95,13 +97,22 @@ extension SingleDirectiveGraphQLDocument {
9597
}
9698
"""
9799
}
100+
98101
let sortedInputs = inputs.sorted { $0.0 < $1.0 }
99-
let inputTypes = sortedInputs.map { "$\($0.key): \($0.value.type)" }.joined(separator: ", ")
100-
let inputParameters = sortedInputs.map { "\($0.key): $\($0.key)" }.joined(separator: ", ")
102+
let variableInputs = sortedInputs.filter { !$0.value.value.isInline() }
103+
let inlineInputs = sortedInputs.filter { $0.value.value.isInline() }
104+
let variableInputTypes = variableInputs.map { "$\($0.key): \($0.value.type)" }.joined(separator: ", ")
105+
106+
var inputParameters = variableInputs.map { ($0.key, "$\($0.key)") }
107+
for input in inlineInputs {
108+
if case .inline(let document) = input.value.value {
109+
inputParameters.append((input.key, document.graphQLInlineValue))
110+
}
111+
}
101112

102113
return """
103-
\(operationType.rawValue) \(name.pascalCased())(\(inputTypes)) {
104-
\(name)(\(inputParameters)) {
114+
\(operationType.rawValue) \(name.pascalCased())\(variableInputTypes.isEmpty ? "" : "(\(variableInputTypes))") {
115+
\(name)(\(inputParameters.map({ "\($0.0): \($0.1)"}).joined(separator: ", "))) {
105116
\(selectionSetString)
106117
}
107118
}

AmplifyPlugins/Core/AWSPluginsCore/Model/Support/GraphQLDocumentnputValue.swift renamed to AmplifyPlugins/Core/AWSPluginsCore/Model/Support/GraphQLDocumentInputValue.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,69 @@ import Foundation
1010
/// A container to hold either an object or a value, useful for storing document inputs and allowing manipulation at
1111
/// the first level of the object
1212
public enum GraphQLDocumentInputValue {
13+
case inline(GraphQLDocumentValueRepresentable)
1314
case scalar(GraphQLDocumentValueRepresentable)
1415
case object([String: Any?])
16+
17+
public func isInline() -> Bool {
18+
if case .inline = self {
19+
return true
20+
}
21+
return false
22+
}
1523
}
1624

1725
public protocol GraphQLDocumentValueRepresentable {
1826
var graphQLDocumentValue: String { get }
27+
var graphQLInlineValue: String { get }
1928
}
2029

2130
extension Int: GraphQLDocumentValueRepresentable {
2231
public var graphQLDocumentValue: String {
2332
return "\(self)"
2433
}
34+
35+
public var graphQLInlineValue: String {
36+
return "\(self)"
37+
}
2538
}
2639

2740
extension String: GraphQLDocumentValueRepresentable {
2841
public var graphQLDocumentValue: String {
2942
return self
3043
}
44+
45+
public var graphQLInlineValue: String {
46+
return "\"\(self)\""
47+
}
3148
}
3249

3350
extension Bool: GraphQLDocumentValueRepresentable {
3451
public var graphQLDocumentValue: String {
3552
return "\(self)"
3653
}
54+
55+
public var graphQLInlineValue: String {
56+
return "\(self)"
57+
}
3758
}
3859

3960
extension Decimal: GraphQLDocumentValueRepresentable {
4061
public var graphQLDocumentValue: String {
4162
return "\(self)"
4263
}
64+
65+
public var graphQLInlineValue: String {
66+
return "\(self)"
67+
}
68+
}
69+
70+
extension Double: GraphQLDocumentValueRepresentable {
71+
public var graphQLDocumentValue: String {
72+
return "\(self)"
73+
}
74+
75+
public var graphQLInlineValue: String {
76+
return "\(self)"
77+
}
4378
}

AmplifyPlugins/Core/AWSPluginsCoreTests/Model/GraphQLRequest/GraphQLRequestSyncCustomPrimaryKeyTests.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ class GraphQLRequestSyncCustomPrimaryKeyTests: XCTestCase {
2929
documentBuilder.add(decorator: ConflictResolutionDecorator(graphQLType: .query))
3030
let document = documentBuilder.build()
3131
let documentStringValue = """
32-
query GetCustomerOrder($id: ID!, $orderId: String!) {
33-
getCustomerOrder(id: $id, orderId: $orderId) {
32+
query GetCustomerOrder {
33+
getCustomerOrder(id: "\(order.id)", orderId: "\(order.orderId)") {
3434
orderId
3535
id
3636
email
@@ -56,8 +56,7 @@ class GraphQLRequestSyncCustomPrimaryKeyTests: XCTestCase {
5656
return
5757
}
5858

59-
XCTAssertEqual(variables["id"] as? String, order.id)
60-
XCTAssertEqual(variables["orderId"] as? String, order.orderId)
59+
XCTAssertTrue(variables.isEmpty)
6160
}
6261

6362
func testCreateMutationGraphQLRequest() throws {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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 Combine
11+
import XCTest
12+
import AWSAPIPlugin
13+
import AWSDataStorePlugin
14+
15+
@testable import Amplify
16+
@testable import DataStoreHostApp
17+
18+
fileprivate struct TestModels: AmplifyModelRegistration {
19+
func registerModels(registry: ModelRegistry.Type) {
20+
ModelRegistry.register(modelType: Post11.self)
21+
}
22+
23+
var version: String = "test"
24+
}
25+
26+
class AWSDataStoreIntSortKeyTest: XCTestCase {
27+
let configFile = "testconfiguration/AWSDataStoreCategoryPluginPrimaryKeyIntegrationTests-amplifyconfiguration"
28+
29+
override func setUp() async throws {
30+
continueAfterFailure = true
31+
let config = try TestConfigHelper.retrieveAmplifyConfiguration(forResource: configFile)
32+
try Amplify.add(plugin: AWSAPIPlugin(
33+
sessionFactory: AmplifyURLSessionFactory())
34+
)
35+
try Amplify.add(plugin: AWSDataStorePlugin(
36+
modelRegistration: TestModels(),
37+
configuration: .custom(syncMaxRecords: 100)
38+
))
39+
Amplify.Logging.logLevel = .verbose
40+
try Amplify.configure(config)
41+
}
42+
43+
override func tearDown() async throws {
44+
try await Amplify.DataStore.clear()
45+
await Amplify.reset()
46+
try await Task.sleep(seconds: 1)
47+
}
48+
49+
func waitDataStoreReady() async throws {
50+
let ready = expectation(description: "DataStore is ready")
51+
var requests: Set<AnyCancellable> = []
52+
Amplify.Hub.publisher(for: .dataStore)
53+
.filter { $0.eventName == HubPayload.EventName.DataStore.ready }
54+
.sink { _ in
55+
ready.fulfill()
56+
}
57+
.store(in: &requests)
58+
59+
try await Amplify.DataStore.start()
60+
await fulfillment(of: [ready], timeout: 60)
61+
}
62+
63+
func testCreateModel_withSortKeyInIntegerType_success() async throws {
64+
try await waitDataStoreReady()
65+
var requests: Set<AnyCancellable> = []
66+
let post = Post11(postId: UUID().uuidString, sk: 15)
67+
let postCreated = expectation(description: "Post is created")
68+
postCreated.assertForOverFulfill = false
69+
Amplify.Hub.publisher(for: .dataStore)
70+
.filter { $0.eventName == HubPayload.EventName.DataStore.syncReceived }
71+
.compactMap { $0.data as? MutationEvent }
72+
.filter { $0.modelId == post.identifier }
73+
.sink { _ in
74+
postCreated.fulfill()
75+
}.store(in: &requests)
76+
77+
try await Amplify.DataStore.save(post)
78+
await fulfillment(of: [postCreated], timeout: 5)
79+
}
80+
81+
func testQueryCreatedModel_withSortKeyInIntegerType_success() async throws {
82+
try await waitDataStoreReady()
83+
var requests: Set<AnyCancellable> = []
84+
let post = Post11(postId: UUID().uuidString, sk: 15)
85+
let postCreated = expectation(description: "Post is created")
86+
postCreated.assertForOverFulfill = false
87+
Amplify.Hub.publisher(for: .dataStore)
88+
.filter { $0.eventName == HubPayload.EventName.DataStore.syncReceived }
89+
.compactMap { $0.data as? MutationEvent }
90+
.filter { $0.modelId == post.identifier }
91+
.sink { _ in
92+
postCreated.fulfill()
93+
}.store(in: &requests)
94+
95+
try await Amplify.DataStore.save(post)
96+
await fulfillment(of: [postCreated], timeout: 5)
97+
98+
let queryResult = try await Amplify.API.query(
99+
request: .get(
100+
Post11.self,
101+
byIdentifier: .identifier(postId: post.postId, sk: post.sk)
102+
)
103+
)
104+
105+
switch queryResult {
106+
case .success(let queriedPost):
107+
XCTAssertEqual(post.identifier, queriedPost!.identifier)
108+
case .failure(let error):
109+
XCTFail("Failed to query comment \(error)")
110+
}
111+
}
112+
113+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// swiftlint:disable all
2+
import Amplify
3+
import Foundation
4+
5+
extension Post11 {
6+
// MARK: - CodingKeys
7+
public enum CodingKeys: String, ModelKey {
8+
case postId
9+
case sk
10+
case createdAt
11+
case updatedAt
12+
}
13+
14+
public static let keys = CodingKeys.self
15+
// MARK: - ModelSchema
16+
17+
public static let schema = defineSchema { model in
18+
let post11 = Post11.keys
19+
20+
model.pluralName = "Post11s"
21+
22+
model.attributes(
23+
.index(fields: ["postId", "sk"], name: nil),
24+
.primaryKey(fields: [post11.postId, post11.sk])
25+
)
26+
27+
model.fields(
28+
.field(post11.postId, is: .required, ofType: .string),
29+
.field(post11.sk, is: .required, ofType: .int),
30+
.field(post11.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime),
31+
.field(post11.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime)
32+
)
33+
}
34+
}
35+
36+
extension Post11: ModelIdentifiable {
37+
public typealias IdentifierFormat = ModelIdentifierFormat.Custom
38+
public typealias IdentifierProtocol = ModelIdentifier<Self, ModelIdentifierFormat.Custom>
39+
}
40+
41+
extension Post11.IdentifierProtocol {
42+
public static func identifier(postId: String,
43+
sk: Int) -> Self {
44+
.make(fields:[(name: "postId", value: postId), (name: "sk", value: sk)])
45+
}
46+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// swiftlint:disable all
2+
import Amplify
3+
import Foundation
4+
5+
public struct Post11: Model {
6+
public let postId: String
7+
public let sk: Int
8+
public var createdAt: Temporal.DateTime?
9+
public var updatedAt: Temporal.DateTime?
10+
11+
public init(postId: String,
12+
sk: Int) {
13+
self.init(postId: postId,
14+
sk: sk,
15+
createdAt: nil,
16+
updatedAt: nil)
17+
}
18+
internal init(postId: String,
19+
sk: Int,
20+
createdAt: Temporal.DateTime? = nil,
21+
updatedAt: Temporal.DateTime? = nil) {
22+
self.postId = postId
23+
self.sk = sk
24+
self.createdAt = createdAt
25+
self.updatedAt = updatedAt
26+
}
27+
}

0 commit comments

Comments
 (0)