Skip to content

Commit 7b5611b

Browse files
authored
Add Embeddable type to store schema info for custom types (#539)
* Add Embbeded types support * Renaming to Embeddable type
1 parent dac0b46 commit 7b5611b

File tree

17 files changed

+436
-19
lines changed

17 files changed

+436
-19
lines changed

Amplify.xcodeproj/project.pbxproj

Lines changed: 43 additions & 7 deletions
Large diffs are not rendered by default.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
// MARK: - Embeddable
11+
12+
/// A `Embeddable` type can be used in a `Model` as an embedded type. All types embedded in a `Model` as an
13+
/// `embedded(type:)` or `embeddedCollection(of:)` must comform to the `Embeddable` protocol except for Swift's Basic
14+
/// types embedded as a collection. A collection of String can be embedded in the `Model` as
15+
/// `embeddedCollection(of: String.self)` without needing to conform to Embeddable.
16+
public protocol Embeddable: Codable {
17+
18+
/// A reference to the `ModelSchema` associated with this embedded type.
19+
static var schema: ModelSchema { get }
20+
}
21+
22+
extension Embeddable {
23+
public static func defineSchema(name: String? = nil,
24+
attributes: ModelAttribute...,
25+
define: (inout ModelSchemaDefinition) -> Void) -> ModelSchema {
26+
var definition = ModelSchemaDefinition(name: name ?? "",
27+
attributes: attributes)
28+
define(&definition)
29+
return definition.build()
30+
}
31+
}

Amplify/Categories/DataStore/Model/Schema/ModelField+Association.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,24 @@ extension ModelField {
183183
return false
184184
}
185185

186+
public var embeddedType: Embeddable.Type? {
187+
switch type {
188+
case .embedded(let type), .embeddedCollection(let type):
189+
if let embeddedType = type as? Embeddable.Type {
190+
return embeddedType
191+
}
192+
return nil
193+
default:
194+
return nil
195+
}
196+
}
197+
198+
public var isEmbeddedType: Bool {
199+
switch type {
200+
case .embedded, .embeddedCollection:
201+
return true
202+
default:
203+
return false
204+
}
205+
}
186206
}

Amplify/Categories/DataStore/Model/Schema/ModelSchema+Definition.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ public enum ModelFieldType {
1919
case timestamp
2020
case bool
2121
case `enum`(type: EnumPersistable.Type)
22-
case customType(_ type: Codable.Type)
22+
case embedded(type: Codable.Type)
23+
case embeddedCollection(of: Codable.Type)
2324
case model(type: Model.Type)
2425
case collection(of: Model.Type)
2526

2627
public var isArray: Bool {
2728
switch self {
28-
case .collection:
29+
case .collection, .embeddedCollection:
2930
return true
3031
default:
3132
return false
@@ -63,8 +64,8 @@ public enum ModelFieldType {
6364
if let modelType = type as? Model.Type {
6465
return .model(type: modelType)
6566
}
66-
if let codableType = type as? Codable.Type {
67-
return .customType(codableType)
67+
if let embeddedType = type as? Codable.Type {
68+
return .embedded(type: embeddedType)
6869
}
6970
preconditionFailure("Could not create a ModelFieldType from \(String(describing: type)) MetaType")
7071
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public struct ConflictResolutionDecorator: ModelBasedGraphQLDocumentDecorator {
5151
/// Append the correct conflict resolution fields for `model` and `pagination` selection sets.
5252
private func addConflictResolution(selectionSet: SelectionSet) {
5353
switch selectionSet.value.fieldType {
54-
case .value:
54+
case .value, .embedded:
5555
break
5656
case .model:
5757
selectionSet.addChild(settingParentOf: .init(value: .init(name: "_version", fieldType: .value)))

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ extension Model {
4949
// TODO how to handle associations of type "many" (i.e. cascade save)?
5050
// This is not supported right now and might be added as a future feature
5151
break
52+
case .embedded, .embeddedCollection:
53+
if let encodable = value as? Encodable {
54+
let jsonEncoder = JSONEncoder(dateEncodingStrategy: ModelDateFormatting.encodingStrategy)
55+
do {
56+
let data = try jsonEncoder.encode(encodable.eraseToAnyEncodable())
57+
input[name] = try JSONSerialization.jsonObject(with: data)
58+
} catch {
59+
preconditionFailure("Could not turn into json object from \(value)")
60+
}
61+
}
5262
default:
5363
input[name] = value
5464
}

AmplifyPlugins/Core/AWSPluginsCore/Model/Support/SelectionSet.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public typealias SelectionSet = Tree<SelectionSetField>
1313
public enum SelectionSetFieldType {
1414
case pagination
1515
case model
16+
case embedded
1617
case value
1718
}
1819

@@ -35,7 +36,11 @@ extension SelectionSet {
3536

3637
func withModelFields(_ fields: [ModelField]) {
3738
fields.forEach { field in
38-
if field.isAssociationOwner, let associatedModel = field.associatedModel {
39+
if field.isEmbeddedType, let embeddedType = field.embeddedType {
40+
let child = SelectionSet(value: .init(name: field.name, fieldType: .embedded))
41+
child.withCodableFields(embeddedType.schema.sortedFields)
42+
self.addChild(settingParentOf: child)
43+
} else if field.isAssociationOwner, let associatedModel = field.associatedModel {
3944
let child = SelectionSet(value: .init(name: field.name, fieldType: .model))
4045
child.withModelFields(associatedModel.schema.graphQLFields)
4146
self.addChild(settingParentOf: child)
@@ -47,6 +52,19 @@ extension SelectionSet {
4752
addChild(settingParentOf: .init(value: .init(name: "__typename", fieldType: .value)))
4853
}
4954

55+
func withCodableFields(_ fields: [ModelField]) {
56+
fields.forEach { field in
57+
if field.isEmbeddedType, let embeddedType = field.embeddedType {
58+
let child = SelectionSet(value: .init(name: field.name, fieldType: .embedded))
59+
child.withCodableFields(embeddedType.schema.sortedFields)
60+
self.addChild(settingParentOf: child)
61+
} else {
62+
self.addChild(settingParentOf: .init(value: .init(name: field.name, fieldType: .value)))
63+
}
64+
}
65+
addChild(settingParentOf: .init(value: .init(name: "__typename", fieldType: .value)))
66+
}
67+
5068
/// Generate the string value of the `SelectionSet` used in the GraphQL query document
5169
///
5270
/// This method operates on `SelectionSet` with the root node containing a nil `value.name` and expects all inner
@@ -68,7 +86,7 @@ extension SelectionSet {
6886
let indent = indentSize == 0 ? "" : String(repeating: " ", count: indentSize)
6987

7088
switch value.fieldType {
71-
case .model, .pagination:
89+
case .model, .pagination, .embedded:
7290
if let name = value.name {
7391
result.append(indent + name + " {")
7492
children.forEach { innerSelectionSetField in

AmplifyPlugins/Core/AWSPluginsCoreTests/Model/Decorator/AuthRuleDecorator/ModelMultipleOwnerAuthRuleTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public struct ModelMultipleOwner: Model {
4949
model.fields(
5050
.id(),
5151
.field(modelMultipleOwner.content, is: .required, ofType: .string),
52-
.field(modelMultipleOwner.editors, is: .optional, ofType: .customType([String].self))
52+
.field(modelMultipleOwner.editors, is: .optional, ofType: .embeddedCollection(of: String.self))
5353
)
5454
}
5555
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import XCTest
9+
10+
@testable import Amplify
11+
@testable import AmplifyTestCommon
12+
@testable import AWSPluginsCore
13+
14+
class GraphQLRequestNonModelTests: XCTestCase {
15+
16+
override func setUp() {
17+
ModelRegistry.register(modelType: Todo.self)
18+
}
19+
20+
override func tearDown() {
21+
ModelRegistry.reset()
22+
}
23+
24+
func testCreateTodoGraphQLRequest() {
25+
let color1 = Color(name: "color1", red: 1, green: 2, blue: 3)
26+
let color2 = Color(name: "color2", red: 12, green: 13, blue: 14)
27+
let category1 = Category(name: "green", color: color1)
28+
let category2 = Category(name: "red", color: color2)
29+
let section = Section(name: "section", number: 1.1)
30+
let todo = Todo(name: "my first todo",
31+
description: "todo description",
32+
categories: [category1, category2],
33+
section: section)
34+
let documentStringValue = """
35+
mutation CreateTodo($input: CreateTodoInput!) {
36+
createTodo(input: $input) {
37+
id
38+
categories {
39+
color {
40+
blue
41+
green
42+
name
43+
red
44+
__typename
45+
}
46+
name
47+
__typename
48+
}
49+
description
50+
name
51+
section {
52+
name
53+
number
54+
__typename
55+
}
56+
stickies
57+
__typename
58+
}
59+
}
60+
"""
61+
let request = GraphQLRequest<Todo>.create(todo)
62+
XCTAssertEqual(documentStringValue, request.document)
63+
64+
guard let variables = request.variables else {
65+
XCTFail("The request doesn't contain variables")
66+
return
67+
}
68+
guard let input = variables["input"] as? [String: Any] else {
69+
XCTFail("The document variables property doesn't contain a valid input")
70+
return
71+
}
72+
XCTAssertEqual(input["id"] as? String, todo.id)
73+
}
74+
}

AmplifyPlugins/Core/AWSPluginsCoreTests/Model/Support/ModelGraphQLTests.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,33 @@ class ModelGraphQLTests: XCTestCase {
4343
XCTAssertTrue(graphQLInput.keys.contains("updatedAt"))
4444
XCTAssertNil(graphQLInput["updatedAt"]!)
4545
}
46+
47+
func testTodoModelToGraphQLInputSuccess() {
48+
let color = Color(name: "red", red: 255, green: 0, blue: 0)
49+
let category = Category(name: "green", color: color)
50+
let todo = Todo(name: "name",
51+
description: "description",
52+
categories: [category],
53+
stickies: ["stickie1"])
54+
55+
let graphQLInput = todo.graphQLInput
56+
57+
XCTAssertEqual(graphQLInput["id"] as? String, todo.id)
58+
XCTAssertEqual(graphQLInput["name"] as? String, todo.name)
59+
XCTAssertEqual(graphQLInput["description"] as? String, todo.description)
60+
guard let categories = graphQLInput["categories"] as? [[String: Any]] else {
61+
XCTFail("Couldn't get array of categories")
62+
return
63+
}
64+
XCTAssertEqual(categories.count, 1)
65+
XCTAssertEqual(categories[0]["name"] as? String, category.name)
66+
guard let expectedColor = categories[0]["color"] as? [String: Any] else {
67+
XCTFail("Couldn't get color in category")
68+
return
69+
}
70+
XCTAssertEqual(expectedColor["name"] as? String, color.name)
71+
XCTAssertEqual(expectedColor["red"] as? Int, color.red)
72+
XCTAssertEqual(expectedColor["green"] as? Int, color.green)
73+
XCTAssertEqual(expectedColor["blue"] as? Int, color.blue)
74+
}
4675
}

0 commit comments

Comments
 (0)