Amplify does not allow for the modification of just one field in the model, we always have to send all of its data. This becomes a significant issue when more than one user is editing the same model, resulting in data being overwritten by each other.
Fortunately, GraphQL requests provide such capability, and Amplify offers us many tools for constructing and sending these requests.
Let’s consider such a model:
public struct Item: Model {
public let id: String
public var name: String
public var modelFile: File?
public var imageFile: File?
}
let myItem = Item(...)
The first thing we need to do is create a mutation GraphQL request:
let request = GraphQLRequest- .updateMutation(of: myItem)
If we were to send the request in this form, our model would be completely modified in the database. Therefore, we need to remove all fields from the request that we don’t want to edit, which are located in the dictionary request.variables?[“input”] – we just need to remove them.
Unfortunately, the fields in the GraphQLRequest structure are constant let, so we need to copy them, edit, and then create our own request instance by referencing everything from the previously generated one:
var input = (request.variables?["input"] as? [String: Any]) ?? [:]
// Do not modify modelFile field
input.removeValue(forKey: Item.CodingKeys.modelFile.stringValue)
let editedRequest = GraphQLRequest(apiName: request.apiName, document: request.document, variables: ["input": input], responseType: request.responseType, decodePath: request.decodePath, options: request.options)
And then we simply need to send it:
let result = try await Amplify.API.mutate(request: editedRequest)
let updatedItem = try result.get().model.instance as? Item
Summarizing everything up to this point, the method for editing the Item model could look like this:
private static func updateItem(item: Item, updates: [Item.CodingKeys : Encodable]) async throws -> Item {
let tmpRequest = GraphQLRequest- .updateMutation(of: item)
var input = (tmpRequest.variables?["input"] as? [String: Any]) ?? [:]
for key in Item.CodingKeys.allCases {
// Do not remove id from input
if key.stringValue == AnyModel.CodingKeys.id.stringValue { continue }
if let newValue = updates[key] {
// UPDATE this value
do {
let newValueData = try JSONEncoder().encode(newValue)
let newValueJson = try JSONSerialization.jsonObject(with: newValueData)
input[key.stringValue] = newValueJson
} catch {
input[key.stringValue] = newValue
}
} else {
// DO NOT UPDATE this value
input.removeValue(forKey: key.stringValue)
}
}
let request = GraphQLRequest
(apiName: tmpRequest.apiName, document: tmpRequest.document, variables: ["input": input], responseType: tmpRequest.responseType, decodePath: tmpRequest.decodePath, options: tmpRequest.options)
let result = try await Amplify.API.mutate(request: request)
guard let newModel = try result.get().model.instance as? Item else {
throw nilResultError
}
return newModel
}
Looks fine, but could it be done more generic? So that we don’t have to write the same code for each model?
Yes! Just mark every model we want to modify as conforming to ‘HasCodingKeys’ protocol and and change our function accordingly:
protocol HasCodindKeys: Model {
associatedtype Keys: ModelKey
static var keys: Keys.Type { get }
}
// Item model conforms to HasCodingKeys protocol
extension Item: HasCodindKeys {}
// Our final implementation:
private static func updateModel(model: T, updates: [T.Keys: Encodable]) async throws -> T {
let tmpRequest = GraphQLRequest.updateMutation(of: model)
var input = (tmpRequest.variables?["input"] as? [String: Any]) ?? [:]
for key in T.Keys.allCases {
// Do not remove id from input
if key.stringValue == AnyModel.CodingKeys.id.stringValue { continue }
if let newValue = updates[key] {
// UPDATE this value
do {
let newValueData = try JSONEncoder().encode(newValue)
let newValueJson = try JSONSerialization.jsonObject(with: newValueData)
input[key.stringValue] = newValueJson
} catch {
input[key.stringValue] = newValue
}
} else {
// DO NOT UPDATE this value
input.removeValue(forKey: key.stringValue)
}
}
let request = GraphQLRequest(apiName: tmpRequest.apiName, document: tmpRequest.document, variables: ["input": input], responseType: tmpRequest.responseType, decodePath: tmpRequest.decodePath, options: tmpRequest.options)
let result = try await Amplify.API.mutate(request: request)
guard let newModel = try result.get().model.instance as? T else {
throw nilResultError
}
return newModel
}
Example updating item’s name and imageFile without changing modelFile:
try await updateItem(item: myItem, updates: [
.name: "New item name",
.imageFile: File(url: url)
]
)
Warning: If you have ‘Conflict resolution’ enabled, you need to provide the current version of the model when creating a request:
let version = try await GraphQL.getModelVersion(model: myItem)
let request = GraphQLRequest- .updateMutation(of: myItem, version: version)
Where the GraphQL.getModelVersion method looks like this:
private static func getModelVersion(model: T) async throws -> Int {
let request = GraphQLRequest.query(modelName: model.modelName, byId: model.identifier)
let result = try await Amplify.API.query(request: request).get()
guard let version = result?.syncMetadata.version else {
throw nilResultError
}
return version
}