Skip to content

Conversation

lawmicha
Copy link
Contributor

@lawmicha lawmicha commented Oct 28, 2022

Table of contents

Instructions

To run this without the feature flag, use the branch from lawmicha.codegen (https://github.com/aws-amplify/amplify-codegen/tree/lawmicha.codegen), run yarn setup-dev and in your project amplify-dev codegen models

Alternatively, for the latest changes,

  1. pull this code locally
  2. Apply this change locally
protected isGenerateModelsForLazyLoadAndCustomSelectionSet(): boolean {
    return true;
    return this.config.generateModelsForLazyLoadAndCustomSelectionSet ?? false;
  }

This is because the feature flag has not been merged in to the CLI repo.
3. run yarn setup-dev
4. Back in your amplify project, use amplify-dev codegen models instead.

Description of Changes

  1. A reference to the associated model in belongs-to or has-one is converted into a computed property that is backed by the LazyReference type

Schema example

type Post4V2 @model @auth(rules: [{allow: public}]) {
  id: ID!
  title: String!
  comments: [Comment4V2] @hasMany(indexName: "byPost4", fields: ["id"])
}

type Comment4V2 @model @auth(rules: [{allow: public}]) {
  id: ID!
  postID: ID @index(name: "byPost4", sortKeyFields: ["content"])
  content: String!
  post: Post4V2 @belongsTo(fields: ["postID"])
}

Before

public var post: Post4V2?

After

internal var _post: LazyReference<Post4V2>
public var post: Post4V2? {
    get async throws {
        try await _post.get()
    }
}
  1. If the associated field is a required property then implementation should call require() instead of get()

If post is required on the comment

internal var _post: LazyReference<Post4V2>
public var post: Post4V2 {
    get async throws {
        try await _post.require()
    }
}

if post is optional on the comment

internal var _post: LazyReference<Post4V2>
public var post: Post4V2 {
    get async throws {
        try await _post.get()
    }
}
  1. The internal init should create an instance of the LazyModel with either the nil associated model or the passed in model.
internal init(id: String = UUID().uuidString,
                content: String,
                post: Post4V2? = nil,
                createdAt: Temporal.DateTime? = nil,
                updatedAt: Temporal.DateTime? = nil) {
    self.id = id
    self.content = content
    self._post = LazyReference(post)
    self.createdAt = createdAt
    self.updatedAt = updatedAt
}
  1. A mutating func allows the developer to update existing queried models to associate them to other models or removal the association (if it allows to be optional)
public mutating func setPost(_ post: Post4V2?) {
    self._post = LazyModel(element: post)
}

developer call pattern:

var queriedComment = try await Amplify.DataStore.query(Comment.self, byId: "commentId")
comment.setPost(post)
comment.setPost(nil)

When the association is required, then nil cannot be passed in to remove the association, it will only allow setting it with another model

public mutating func setPost(_ post: Post4V2) {
    self._post = LazyModel(element: post)
}
  1. Custom encoder and decoder, its purpose is to create the internal LazyReference properties successfully, while exposing the coding as “post” in the model swift schema file. The model instance is also encoded back into the object containing the coding key “post”.
public init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    id = try values.decode(String.self, forKey: .id)
    content = try values.decode(String.self, forKey: .content)
     _post = try values.decode(LazyModel<Post4V2>.self, forKey: .post)
    createdAt = try values.decode(Temporal.DateTime?.self, forKey: .createdAt)
    updatedAt = try values.decode(Temporal.DateTime?.self, forKey: .updatedAt)
}

public func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(id, forKey: .id)
    try container.encode(content, forKey: .content)
    try container.encode(_post, forKey: .post)
    try container.encode(createdAt, forKey: .createdAt)
    try container.encode(updatedAt, forKey: .updatedAt)
}
  1. Add static rootPath (In the swift schema file)
extension Comment4V2 {
    // ...
    
    public class Path: ModelPath<Comment4V2> { }

    public static var rootPath: PropertyContainerPath? { Path() }
}
  1. Add ModelPath
extension ModelPath where ModelType == Comment4V2 {
    var id: FieldPath<String> { id() }
    var content: FieldPath<String> { string("content") }
    var post: ModelPath<Post4V2> { Post4V2.Path(name: "post", parent: self) }
    var createdAt: FieldPath<Temporal.DateTime> { datetime("createdAt") }
    var updatedAt: FieldPath<Temporal.DateTime> { datetime("updatedAt") }
}

Full Schema Example

Post Swift file

// swiftlint:disable all
import Amplify
import Foundation

public struct Post4V2: Model {
  public let id: String
  public var title: String
  public var comments: List<Comment4V2>?
  public var createdAt: Temporal.DateTime?
  public var updatedAt: Temporal.DateTime?

  public init(id: String = UUID().uuidString,
      title: String,
      comments: List<Comment4V2>? = []) {
    self.init(id: id,
      title: title,
      comments: comments,
      createdAt: nil,
      updatedAt: nil)
  }
  internal init(id: String = UUID().uuidString,
      title: String,
      comments: List<Comment4V2>? = [],
      createdAt: Temporal.DateTime? = nil,
      updatedAt: Temporal.DateTime? = nil) {
      self.id = id
      self.title = title
      self.comments = comments
      self.createdAt = createdAt
      self.updatedAt = updatedAt
  }
}

Post Swift Schema file

// swiftlint:disable all
import Amplify
import Foundation

extension Post4V2 {
  // MARK: - CodingKeys 
   public enum CodingKeys: String, ModelKey {
    case id
    case title
    case comments
    case createdAt
    case updatedAt
  }

  public static let keys = CodingKeys.self
  //  MARK: - ModelSchema 
  
  public static let schema = defineSchema { model in
    let post4V2 = Post4V2.keys

    model.authRules = [
      rule(allow: .public, operations: [.create, .update, .delete, .read])
    ]

    model.pluralName = "Post4V2s"

    model.attributes(
      .primaryKey(fields: [post4V2.id])
    )

    model.fields(
      .field(post4V2.id, is: .required, ofType: .string),
      .field(post4V2.title, is: .required, ofType: .string),
      .hasMany(post4V2.comments, is: .optional, ofType: Comment4V2.self, associatedWith: Comment4V2.keys.post),
      .field(post4V2.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime),
      .field(post4V2.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime)
    )
    }

    public class Path: ModelPath<Post4V2> { }

    public static var rootPath: PropertyContainerPath? { Path() }
}

extension Post4V2: ModelIdentifiable {
  public typealias IdentifierFormat = ModelIdentifierFormat.Default
  public typealias IdentifierProtocol = DefaultModelIdentifier<Self>
}

extension ModelPath where ModelType == Post4V2 {
    var id: FieldPath<String> { id() }
    var title: FieldPath<String> { string("title") }
    var comments: ModelPath<Comment4V2> { Comment4V2.Path(name: "comments", isCollection: true, parent: self) }
    var createdAt: FieldPath<Temporal.DateTime> { datetime("createdAt") }
    var updatedAt: FieldPath<Temporal.DateTime> { datetime("updatedAt") }
}

Comment Swift file

// swiftlint:disable all
import Amplify
import Foundation

public struct Comment4V2: Model {
    public let id: String
    public var content: String
    internal var _post: LazyReference<Post4V2>
    public var post: Post4V2? {
        get async throws {
            try await _post.get()
        }
    }
    public var createdAt: Temporal.DateTime?
    public var updatedAt: Temporal.DateTime?

    public init(id: String = UUID().uuidString,
                content: String,
                post: Post4V2? = nil) {
        self.init(id: id,
                  content: content,
                  post: post,
                  createdAt: nil,
                  updatedAt: nil)
    }
    internal init(id: String = UUID().uuidString,
                  content: String,
                  post: Post4V2? = nil,
                  createdAt: Temporal.DateTime? = nil,
                  updatedAt: Temporal.DateTime? = nil) {
        self.id = id
        self.content = content
        self._post = LazyReference(post)
        self.createdAt = createdAt
        self.updatedAt = updatedAt
    }

    public mutating func setPost(_ post: Post4V2? = nil) {
        self._post = LazyReference(post)
    }

    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(String.self, forKey: .id)
        content = try values.decode(String.self, forKey: .content)
         _post = try values.decode(LazyReference<Post4V2>.self, forKey: .post)
        createdAt = try values.decode(Temporal.DateTime?.self, forKey: .createdAt)
        updatedAt = try values.decode(Temporal.DateTime?.self, forKey: .updatedAt)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(content, forKey: .content)
        try container.encode(_post, forKey: .post)
        try container.encode(createdAt, forKey: .createdAt)
        try container.encode(updatedAt, forKey: .updatedAt)
    }
}

Comment Schema file

// swiftlint:disable all
import Amplify
import Foundation

extension Comment4V2 {
  // MARK: - CodingKeys 
   public enum CodingKeys: String, ModelKey {
    case id
    case content
    case post
    case createdAt
    case updatedAt
  }

  public static let keys = CodingKeys.self
  //  MARK: - ModelSchema 
  
  public static let schema = defineSchema { model in
    let comment4V2 = Comment4V2.keys

    model.authRules = [
      rule(allow: .public, operations: [.create, .update, .delete, .read])
    ]

    model.pluralName = "Comment4V2s"

    model.attributes(
      .index(fields: ["postID", "content"], name: "byPost4"),
      .primaryKey(fields: [comment4V2.id])
    )

    model.fields(
      .field(comment4V2.id, is: .required, ofType: .string),
      .field(comment4V2.content, is: .required, ofType: .string),
      .belongsTo(comment4V2.post, is: .optional, ofType: Post4V2.self, targetNames: ["postID"]),
      .field(comment4V2.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime),
      .field(comment4V2.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime)
    )
    }

    public class Path: ModelPath<Comment4V2> { }

    public static var rootPath: PropertyContainerPath? { Path() }
}

extension Comment4V2: ModelIdentifiable {
  public typealias IdentifierFormat = ModelIdentifierFormat.Default
  public typealias IdentifierProtocol = DefaultModelIdentifier<Self>
}

extension ModelPath where ModelType == Comment4V2 {
    var id: FieldPath<String> { id() }
    var content: FieldPath<String> { string("content") }
    var post: ModelPath<Post4V2> { Post4V2.Path(name: "post", parent: self) }
    var createdAt: FieldPath<Temporal.DateTime> { datetime("createdAt") }
    var updatedAt: FieldPath<Temporal.DateTime> { datetime("updatedAt") }
}

Issue #, if available

Description of how you validated changes

Checklist

  • PR description included
  • yarn test passes
  • Tests are changed or added
  • Relevant documentation is changed or added (and PR referenced)
  • Breaking changes to existing customers are released behind a feature flag or major version update
  • Changes are tested using sample applications for all relevant platforms (iOS/android/flutter/Javascript) that use the feature added/modified

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@lawmicha lawmicha changed the title feat(amplify-codegen): iOS codegen LazyModel type feat(amplify-codegen): iOS LazyReference and ModelPath Nov 18, 2022
@lawmicha lawmicha force-pushed the lawmicha/ios-lazymodel branch from b86af71 to 94cc68b Compare December 1, 2022 16:24
@codecov-commenter
Copy link

codecov-commenter commented Dec 1, 2022

Codecov Report

Merging #504 (1bf0601) into main (7ea98f3) will increase coverage by 0.17%.
The diff coverage is 96.00%.

❗ Current head 1bf0601 differs from pull request most recent head 1c6f4eb. Consider uploading reports for the commit 1c6f4eb to get more accurate results

📣 This organization is not using Codecov’s GitHub App Integration. We recommend you install it so Codecov can continue to function properly for your repositories. Learn more

@@            Coverage Diff             @@
##             main     #504      +/-   ##
==========================================
+ Coverage   85.51%   85.69%   +0.17%     
==========================================
  Files         148      148              
  Lines        7263     7374     +111     
  Branches     1909     1958      +49     
==========================================
+ Hits         6211     6319     +108     
- Misses        956      959       +3     
  Partials       96       96              
Impacted Files Coverage Δ
...elgen-plugin/src/visitors/appsync-swift-visitor.ts 96.70% <95.37%> (-0.58%) ⬇️
packages/amplify-codegen/src/commands/models.js 87.30% <100.00%> (+0.10%) ⬆️
...en-plugin/src/languages/swift-declaration-block.ts 92.07% <100.00%> (+1.62%) ⬆️
...nc-modelgen-plugin/src/visitors/appsync-visitor.ts 96.26% <100.00%> (+0.01%) ⬆️

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@lawmicha lawmicha marked this pull request as ready for review December 5, 2022 22:16
@lawmicha lawmicha requested a review from a team as a code owner December 5, 2022 22:16
@dpilch dpilch mentioned this pull request Dec 6, 2022
3 tasks
Copy link
Contributor

@AaronZyLee AaronZyLee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed previously, we need a feature flag or a library version check for ios lazy loading. In either case we need to keep both old and new codegen implementations. This is a TODO for the PR. Apart from it, I have some comments below.

`self._${fieldName} = LazyReference(${fieldName})`,
[
{
name: `_ ${this.getFieldName(field)}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice there is a space after the underline. Is this space intentional or should it be _${this.getFieldName(field)} instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thsi is intentional for the mutating function:

public mutating func setPost(_ post: Post4V2?) {
    self._post = LazyModel(element: post)
}

the _ represents the unnamed parameter to avoid duplication of "post" in the call site. Developers don't have to specific that it is a post that they're passing in:setPost(post). without it, will be setPost(post: post)

Copy link
Contributor

@phani-srikar phani-srikar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from the Feature Flag thing that we discussed offline, one general question I have is that I see conditional changes based on ConnectionIsHasOneOrBelongsTo but nothing for HasManyOrBelongsTo. Can you please explain the disparity or intention?

@lawmicha
Copy link
Contributor Author

lawmicha commented Dec 9, 2022

.. conditional changes based on ConnectionIsHasOneOrBelongsTo but nothing for HasManyOrBelongsTo. Can you please explain the disparity or intention?

@phani-srikar This is because LazyReference only encapsulates hasOne and belongsTo associations. We have already shipped iOS support for "Lazy" list that is the List type, which is why this PR only has checks for hasOne and belongsTo to add the LazyReference encapsulation

phani-srikar
phani-srikar previously approved these changes Dec 9, 2022
AaronZyLee
AaronZyLee previously approved these changes Dec 12, 2022
Copy link
Contributor

@AaronZyLee AaronZyLee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ⚽

@lawmicha lawmicha force-pushed the lawmicha/ios-lazymodel branch from 1bf0601 to 1c6f4eb Compare January 25, 2023 15:50
@lawmicha
Copy link
Contributor Author

lawmicha commented Jan 25, 2023

Rebased from main which included graphql 15 upgrade, so this code needs to run ontop of two more changes:
aws-amplify/amplify-category-api#1072
aws-amplify/amplify-cli#11751

Our options are

  1. Create a tagged release of this codegen PR, and a tagged release of chore: upgrade to graphql 15 and upgrade data deps amplify-cli#11751

npm i -g @aws-amplify/[email protected]

amplify -v
10.7.0-upgrade-graphql15-lazyload.0
  1. Create a tagged release of a codegen PR without the graphQL 15 changes, thus it will work with latest CLI with FF released.

npm i -g @aws-amplify/[email protected]

amplify --v
10.7.0-lazyload-no-graphql15.0

Update the amplify project's cli.json's generatemodelsforlazyloadandcustomselectionset to true before running amplify codegen models

AaronZyLee
AaronZyLee previously approved these changes Jan 25, 2023
phani-srikar
phani-srikar previously approved these changes Jan 25, 2023
@lawmicha lawmicha dismissed stale reviews from phani-srikar and AaronZyLee via 6145ece January 25, 2023 22:50
@lawmicha
Copy link
Contributor Author

This can be merged in! thanks for the review @AaronZyLee @phani-srikar @drochetti 🙌

@AaronZyLee AaronZyLee merged commit 6ff41e8 into main Jan 25, 2023
`self._${fieldName} = LazyReference(${fieldName})`,
[
{
name: `_ ${this.getFieldName(field)}`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reference: _ sets the call site to not require the parameter name

@alharris-at alharris-at deleted the lawmicha/ios-lazymodel branch July 10, 2023 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants