Initializing selection set manually without using _dataDict initializer

We’re migrating from Apollo 0.x to 1.22. We need to initialize Apollo classes manually, because we intercept and “fake” some of GQL endpoint responses. I’m currently using this in Apollo 1.22:

let itemDetail = itemDTO.toGQLModel()
RenameItemMutation.Data.RenameItem
    return RenameItemMutation.Data(
      renameItem: RenameItemMutation.Data.RenameItem(
        _dataDict: itemDetail.__data
      )
    )

This works properly. However _dataDict initializer is for internal use, and I’d like to know if there is a cleaner way to create RenameItemMutation.Data.RenameItem and ItemDetail. For these particular classes there are no typesafe initializers generated.

We have set selectionSetInitializers configuration for both operations and named fragments.

itemDTO.toGQLModel() impl
extension ItemDTO {
    func toGQLModel() -> ItemDetail {
        switch self {
        case .file(let file):
         // also having to use _dataDict initializer here
            return .init(_dataDict: file.toGQLModel().__data)

        case .folder(let folder):
            return .init(_dataDict: folder.toGQLModel().__data)

        case .webLink(let weblink):
            return .init(_dataDict: weblink.toGQLModel().__data)
        }
    }
}

extension FileDTO {
    func toGQLModel() -> ItemDetail.AsFile {
        return ItemDetail.AsFile(
            id: .init(id),
            type: .init(.file),
            name: name,
            sha1: sha1,
            [...] // more fields
        )
    }
}

// here we can use type-safe initializers
extension FolderDTO {
    func toGQLModel() -> ItemDetail.AsFolder {
        ItemDetail.AsFolder(
            id: .init(id),
            type: .init(.folder),
            name: name,
            createdAt: createdAt?.utcString,
            [...] // more fields
        )
    }
}

extension WebLinkDTO {
    func toGQLModel() -> ItemDetail.AsWeblink {
        ItemDetail.AsWeblink(
            id: .init(id),
            type: .init(.webLink),
            name: name,
            url: url,
            [...] // more fields
        )
    }
}

gql definitions
mutation RenameItem(
    $id: String!,
    $type: ItemType!,
    $newName: String!,
) {
    renameItem(input: {
        itemId: $id,
        type: $type,
        newName: $newName,
    }) {
        ...ItemDetail
    }
}
fragment ItemDetail on Item {
    ...on CoreItem {
        ...CoreItemDetail
    }
    ...on File {
        ...FileDetail
    }
    ...on Folder {
        ...FolderDetail
    }
    ...on Weblink {
        ...WeblinkDetail
    }
}
fragment FileDetail on File { //similar for Folder, Weblink
    size
    hasCollaborations
    isExternallyOwned
    sha1
    [...] more fields
}

Question: Can we do better to avoid using the internal initializer forRenameItemMutation.Data.RenameItem and ItemDetail?

Let me know if you would need more details.

Ideally, you would use selectionSetInitializers. I’m confused why those aren’t being generated for these types if you have the configuration option enabled.

Thanks for the reply. Are you sure the structure we are using is supported by selectionSetInitializers? Thinking because item is an union, and I’m not sure how the initializer would look like.

Would you expect something like: RenameItemMutation.Data.RenameItem(initFrom: ItemDetail.AsFile) generated? With more cases for other union types (Folder/Weblink). Or an initializer with all three asFile, asFolder, asWeblink nullable parameters (pretty sure apollo kotlin does it this way).

union definition in schema (referenced in the original post):

union Item = File | Folder | Weblink

I noticed the schema defines an input as well:

type Mutation {
renameItem(input: RenameItemInput!): Item
}
input RenameItemInput {
itemId: String!
newName: String!
type: ItemType!
}

I can provide any more details you would need.

Anthony,

Also, in the Migration guide for 1.0 - the section on Local Cache Mutation, the example uses

  let newPost = AddUserPostLocalCacheMutation.Data.LoggedInUser.Post(data: DataDict(
    ["__typename": "Post", "id": "789", "body": "This is a new post!"],
    variables: nil
  ))

That too isn’t a very friendly technique.

update: We managed to use asRootEntityType property to initialize ItemDetail :

toGQLModel() impl (refer to original post) improved
extension BoxItemDTO {
    func toGQLModel() -> BoxGQLAPI.ItemDetail {
        switch self {
        case .file(let file):
            return file.toGQLModel().asRootEntityType

        case .folder(let folder):
            return folder.toGQLModel().asRootEntityType

        case .webLink(let weblink):
            return weblink.toGQLModel().asRootEntityType
        }
    }
}

However still we did not solve using _dataDict initializer for RenameItem:

Remaining \_datadict usage
    let itemDetail = value.toGQLModel()
    return BoxGQLAPI.RenameItemMutation.Data(
        renameItem: BoxGQLAPI.RenameItemMutation.Data.RenameItem(
            _dataDict: itemDetail.__data
        )
    )

Using asRootEntityType here is definitely the correct solution. I’d like to find a way to surface that property to users better. We are about to start updating documentation for 2.0, so I’ll try to improve on this. But from an API standpoint, I’m not sure how much we can improve upon this. Open to ideas and suggestions!

But for the RenameItem, I’m still pretty confused on why this isn’t generating an initializer.

We could add a function RootSelectionSet.init<T: TypeCase>(_ childTypeCase: T) where T.RootEntityType = Self that would just call asRootEntityType() internally.

This would make it so you could do:

extension BoxItemDTO {
    func toGQLModel() -> BoxGQLAPI.ItemDetail {
        switch self {
        case .file(let file):
            return BoxGQLAPI.ItemDetail(file.toGQLModel())

        case .folder(let folder):
            return BoxGQLAPI.ItemDetail(folder.toGQLModel())

        case .webLink(let weblink):
            return BoxGQLAPI.ItemDetail(weblink.toGQLModel())
        }
    }
}

Still manually have to do the conversions, but it might be more intuitive for users to convert via an initializer, helping discoverability of the functionality. What do you think?

That looks good.
User needs to initialize ItemDetail (or whatever generated class) - then they look for initializers and what options are available. That’s what I’ve been doing when writing this code.

1 Like

PR for this has been merged. It will go out with the next release of Apollo iOS 1.0. It will also be included in the next beta for 2.0 (Beta 3).

1 Like