Skip to content

Add Embeddable type to store schema info for custom types #539

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

Merged
merged 2 commits into from
Jun 16, 2020
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
50 changes: 43 additions & 7 deletions Amplify.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions Amplify/Categories/DataStore/Model/Embedded.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

// MARK: - Embeddable

/// A `Embeddable` type can be used in a `Model` as an embedded type. All types embedded in a `Model` as an
/// `embedded(type:)` or `embeddedCollection(of:)` must comform to the `Embeddable` protocol except for Swift's Basic
/// types embedded as a collection. A collection of String can be embedded in the `Model` as
/// `embeddedCollection(of: String.self)` without needing to conform to Embeddable.
public protocol Embeddable: Codable {

/// A reference to the `ModelSchema` associated with this embedded type.
static var schema: ModelSchema { get }
}

extension Embeddable {
public static func defineSchema(name: String? = nil,
attributes: ModelAttribute...,
define: (inout ModelSchemaDefinition) -> Void) -> ModelSchema {
var definition = ModelSchemaDefinition(name: name ?? "",
attributes: attributes)
define(&definition)
return definition.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,24 @@ extension ModelField {
return false
}

public var embeddedType: Embeddable.Type? {
switch type {
case .embedded(let type), .embeddedCollection(let type):
if let embeddedType = type as? Embeddable.Type {
return embeddedType
}
return nil
default:
return nil
}
}

public var isEmbeddedType: Bool {
switch type {
case .embedded, .embeddedCollection:
return true
default:
return false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ public enum ModelFieldType {
case timestamp
case bool
case `enum`(type: EnumPersistable.Type)
case customType(_ type: Codable.Type)
case embedded(type: Codable.Type)
case embeddedCollection(of: Codable.Type)
case model(type: Model.Type)
case collection(of: Model.Type)

public var isArray: Bool {
switch self {
case .collection:
case .collection, .embeddedCollection:
return true
default:
return false
Expand Down Expand Up @@ -63,8 +64,8 @@ public enum ModelFieldType {
if let modelType = type as? Model.Type {
return .model(type: modelType)
}
if let codableType = type as? Codable.Type {
return .customType(codableType)
if let embeddedType = type as? Codable.Type {
return .embedded(type: embeddedType)
}
preconditionFailure("Could not create a ModelFieldType from \(String(describing: type)) MetaType")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public struct ConflictResolutionDecorator: ModelBasedGraphQLDocumentDecorator {
/// Append the correct conflict resolution fields for `model` and `pagination` selection sets.
private func addConflictResolution(selectionSet: SelectionSet) {
switch selectionSet.value.fieldType {
case .value:
case .value, .embedded:
break
case .model:
selectionSet.addChild(settingParentOf: .init(value: .init(name: "_version", fieldType: .value)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ extension Model {
// TODO how to handle associations of type "many" (i.e. cascade save)?
// This is not supported right now and might be added as a future feature
break
case .embedded, .embeddedCollection:
if let encodable = value as? Encodable {
let jsonEncoder = JSONEncoder(dateEncodingStrategy: ModelDateFormatting.encodingStrategy)
do {
let data = try jsonEncoder.encode(encodable.eraseToAnyEncodable())
input[name] = try JSONSerialization.jsonObject(with: data)
} catch {
preconditionFailure("Could not turn into json object from \(value)")
}
}
default:
input[name] = value
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public typealias SelectionSet = Tree<SelectionSetField>
public enum SelectionSetFieldType {
case pagination
case model
case embedded
case value
}

Expand All @@ -35,7 +36,11 @@ extension SelectionSet {

func withModelFields(_ fields: [ModelField]) {
fields.forEach { field in
if field.isAssociationOwner, let associatedModel = field.associatedModel {
if field.isEmbeddedType, let embeddedType = field.embeddedType {
let child = SelectionSet(value: .init(name: field.name, fieldType: .embedded))
child.withCodableFields(embeddedType.schema.sortedFields)
self.addChild(settingParentOf: child)
} else if field.isAssociationOwner, let associatedModel = field.associatedModel {
let child = SelectionSet(value: .init(name: field.name, fieldType: .model))
child.withModelFields(associatedModel.schema.graphQLFields)
self.addChild(settingParentOf: child)
Expand All @@ -47,6 +52,19 @@ extension SelectionSet {
addChild(settingParentOf: .init(value: .init(name: "__typename", fieldType: .value)))
}

func withCodableFields(_ fields: [ModelField]) {
fields.forEach { field in
if field.isEmbeddedType, let embeddedType = field.embeddedType {
let child = SelectionSet(value: .init(name: field.name, fieldType: .embedded))
child.withCodableFields(embeddedType.schema.sortedFields)
self.addChild(settingParentOf: child)
} else {
self.addChild(settingParentOf: .init(value: .init(name: field.name, fieldType: .value)))
}
}
addChild(settingParentOf: .init(value: .init(name: "__typename", fieldType: .value)))
}

/// Generate the string value of the `SelectionSet` used in the GraphQL query document
///
/// This method operates on `SelectionSet` with the root node containing a nil `value.name` and expects all inner
Expand All @@ -68,7 +86,7 @@ extension SelectionSet {
let indent = indentSize == 0 ? "" : String(repeating: " ", count: indentSize)

switch value.fieldType {
case .model, .pagination:
case .model, .pagination, .embedded:
if let name = value.name {
result.append(indent + name + " {")
children.forEach { innerSelectionSetField in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public struct ModelMultipleOwner: Model {
model.fields(
.id(),
.field(modelMultipleOwner.content, is: .required, ofType: .string),
.field(modelMultipleOwner.editors, is: .optional, ofType: .customType([String].self))
.field(modelMultipleOwner.editors, is: .optional, ofType: .embeddedCollection(of: String.self))
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest

@testable import Amplify
@testable import AmplifyTestCommon
@testable import AWSPluginsCore

class GraphQLRequestNonModelTests: XCTestCase {

override func setUp() {
ModelRegistry.register(modelType: Todo.self)
}

override func tearDown() {
ModelRegistry.reset()
}

func testCreateTodoGraphQLRequest() {
let color1 = Color(name: "color1", red: 1, green: 2, blue: 3)
let color2 = Color(name: "color2", red: 12, green: 13, blue: 14)
let category1 = Category(name: "green", color: color1)
let category2 = Category(name: "red", color: color2)
let section = Section(name: "section", number: 1.1)
let todo = Todo(name: "my first todo",
description: "todo description",
categories: [category1, category2],
section: section)
let documentStringValue = """
mutation CreateTodo($input: CreateTodoInput!) {
createTodo(input: $input) {
id
categories {
color {
blue
green
name
red
__typename
}
name
__typename
}
description
name
section {
name
number
__typename
}
stickies
__typename
}
}
"""
let request = GraphQLRequest<Todo>.create(todo)
XCTAssertEqual(documentStringValue, request.document)

guard let variables = request.variables else {
XCTFail("The request doesn't contain variables")
return
}
guard let input = variables["input"] as? [String: Any] else {
XCTFail("The document variables property doesn't contain a valid input")
return
}
XCTAssertEqual(input["id"] as? String, todo.id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,33 @@ class ModelGraphQLTests: XCTestCase {
XCTAssertTrue(graphQLInput.keys.contains("updatedAt"))
XCTAssertNil(graphQLInput["updatedAt"]!)
}

func testTodoModelToGraphQLInputSuccess() {
let color = Color(name: "red", red: 255, green: 0, blue: 0)
let category = Category(name: "green", color: color)
let todo = Todo(name: "name",
description: "description",
categories: [category],
stickies: ["stickie1"])

let graphQLInput = todo.graphQLInput

XCTAssertEqual(graphQLInput["id"] as? String, todo.id)
XCTAssertEqual(graphQLInput["name"] as? String, todo.name)
XCTAssertEqual(graphQLInput["description"] as? String, todo.description)
guard let categories = graphQLInput["categories"] as? [[String: Any]] else {
XCTFail("Couldn't get array of categories")
return
}
XCTAssertEqual(categories.count, 1)
XCTAssertEqual(categories[0]["name"] as? String, category.name)
guard let expectedColor = categories[0]["color"] as? [String: Any] else {
XCTFail("Couldn't get color in category")
return
}
XCTAssertEqual(expectedColor["name"] as? String, color.name)
XCTAssertEqual(expectedColor["red"] as? Int, color.red)
XCTAssertEqual(expectedColor["green"] as? Int, color.green)
XCTAssertEqual(expectedColor["blue"] as? Int, color.blue)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public struct SQLiteModelValueConverter: ModelValueConverter {
// collections are not converted to SQL Binding since they represent a model association
// and the foreign key lives on the other side of the association
return nil
case .customType:
case .embedded, .embeddedCollection:
if let encodable = value as? Encodable {
return try SQLiteModelValueConverter.toJSON(encodable)
}
Expand Down Expand Up @@ -77,7 +77,7 @@ public struct SQLiteModelValueConverter: ModelValueConverter {
return nil
case .enum:
return value as? String
case .customType:
case .embedded, .embeddedCollection:
if let stringValue = value as? String {
return try SQLiteModelValueConverter.fromJSON(stringValue)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ extension ExampleWithEveryType {
.field(example.boolField, is: .required, ofType: .bool),
.field(example.dateField, is: .required, ofType: .date),
.field(example.enumField, is: .required, ofType: .enum(type: ExampleEnum.self)),
.field(example.nonModelField, is: .required, ofType: .customType(ExampleNonModelType.self)),
.field(example.arrayOfStringsField, is: .required, ofType: .customType([String].self))
.field(example.nonModelField, is: .required, ofType: .embedded(type: ExampleNonModelType.self)),
.field(example.arrayOfStringsField, is: .required, ofType: .embeddedCollection(of: [String].self))
)
}

Expand Down
31 changes: 31 additions & 0 deletions AmplifyTestCommon/Models/NonModel/Category.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

// swiftlint:disable all
import Amplify
import Foundation

public struct Category: Embeddable {
var name: String
var color: Color
}

extension Category {

public enum CodingKeys: CodingKey {
case name
case color
}

public static let keys = CodingKeys.self

public static let schema = defineSchema { embedded in
let category = Category.keys
embedded.fields(.field(category.name, is: .required, ofType: .string),
.field(category.color, is: .required, ofType: .embedded(type: Color.self)))
}
}
36 changes: 36 additions & 0 deletions AmplifyTestCommon/Models/NonModel/Color.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

// swiftlint:disable all
import Amplify
import Foundation

public struct Color: Embeddable {
var name: String
var red: Int
var green: Int
var blue: Int
}

extension Color {
public enum CodingKeys: CodingKey {
case name
case red
case green
case blue
}

public static let keys = CodingKeys.self

public static let schema = defineSchema { embedded in
let color = Color.keys
embedded.fields(.field(color.name, is: .required, ofType: .string),
.field(color.red, is: .required, ofType: .int),
.field(color.green, is: .required, ofType: .int),
.field(color.blue, is: .required, ofType: .int))
}
}
Loading