I am using AWS AppSync for mobile development (iOS) for offline/Online Capabilities
I am trying to save data in offline mode. But I am getting error "Variable id was not provided/ Missing value"
When app comes to online it automatically syncing to DynamoDB but the issue is only in offline mode unable to fetch saved record
Here is the code using in the application
`
let userObjInput = userObjectInput(id: "id", firstName: "firstname", lastName: "lastName")
let CategoryInputs = CreateUserCategoryInput(categoryName: "categoryValue" , user: userObjInput)
let mutation = CategoryMutation(input: CategoryInputs)
appSyncClient?.perform(mutation: mutation, queue: .main, optimisticUpdate: { (transaction) in
do {
let selectionSets = try transaction?.read(query: query)
try transaction?.update(query: GetUserCategoriesOfUserQuery(id: "id")) { (data: inout GetUserCategoriesOfUserQuery.Data) in
data.getAllCategoriesForUser?.append(GetUserCategoriesOfUserQuery.Data.GetAllCategoriesForUser?.init(GetUserCategoriesOfUserQuery.Data.GetAllCategoriesForUser.init(id: (UUID().uuidString), categoryName: CategoryInputs.categoryName!, isDeleted: false, user: GetUserCategoriesOfUserQuery.Data.GetAllCategoriesForUser.User?.init(GetUserCategoriesOfUserQuery.Data.GetAllCategoriesForUser.User.init(id: userObjInput.id!, firstName: userObjInput.firstName!, lastName: userObjInput.lastName!)))))
} catch {
print(error.localizedDescription)
}
}, conflictResolutionBlock: nil, resultHandler: { (result, error) in
if error == nil {
fetchCategories()
} else {
print(error?.localizedDescription)
}
})`
For those who have problem with optimistic UI missing value. I've found one trick to temporary solve the issue by passing parameter using Custom Request Header from client app.
Before, your query would look like this allDiaries(author: String): [Diary]
Just change it to => allDiaries: [Diary]
So your request mapping would look like below:
{
"version" : "2017-02-28",
"operation" : "Scan",
"filter" : {
"expression" : "author = :author",
"expressionValues" : {
":author" : { "S" : "$context.request.headers.author" }
}
}
}
Reference: How to pass AWS AppSync custom request header in iOS client?
Hope it is useful! Goodluck :)
Related
I know this question is asked a lot, but I can't figure out how to apply any answers to my program. Sorry in advance this async stuff makes absolutely zero sense to me.
Basically, I have a button in SwiftUI that, when pressed, calls a function that makes two API calls to Google Sheets using Alamofire and GoogleSignIn.
Button("Search") {
if fullName != "" {
print(SheetsAPI.nameSearch(name: fullName, user: vm.getUser()) ?? "Error")
}
}
This function should return the values of some cells on success or nil on an error. However, it only ever prints out "Error". Here is the function code.
static func nameSearch<S: StringProtocol>(name: S, advisory: S = "", user: GIDGoogleUser?) -> [String]? {
let name = String(name)
let advisory = String(advisory)
let writeRange = "'App Control'!A2:C2"
let readRange = "'App Control'!A4:V4"
// This function can only ever run when user is logged in, ! should be fine?
let user = user!
let parameters: [String: Any] = [
"range": writeRange,
"values": [
[
name,
nil,
advisory
]
]
]
// What I want to be returned
var data: [String]?
// Google Identity said use this wrapper so that the OAuth tokens refresh
user.authentication.do { authentication, error in
guard error == nil else { return }
guard let authentication = authentication else { return }
// Get the access token to attach it to a REST or gRPC request.
let token = authentication.accessToken
let headers: HTTPHeaders = ["Authorization": "Bearer \(token)"]
AF.request("url", method: .put, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseString { response in
switch response.result {
case .success:
// I assume there is a better way to make two API calls...
AF.request("anotherURL", headers: headers).responseDecodable(of: NameResponseModel.self) { response2 in
switch response2.result {
case .success:
guard let responseData = response2.value else { return }
data = responseData.values[0]
// print(responseData.values[0]) works fine
case .failure:
print(response2.error ?? "Unknown error.")
data = nil
}
}
case .failure:
print(response.error ?? "Unknown error.")
data = nil
}
}
}
// Always returns nil, "Unknown error." never printed
return data
}
The model struct for my second AF request:
struct NameResponseModel: Decodable { let values: [[String]] }
An example API response for the second AF request:
{
"range": "'App Control'!A4:V4",
"majorDimension": "ROWS",
"values": [
[
"Bob Jones",
"A1234",
"Cathy Jones",
"1234 N. Street St. City, State 12345"
]
]
}
I saw stuff about your own callback function as a function parameter (or something along those lines) to handle this, but I was completely lost. I also looked at Swift async/await, but I don't know how that works with callback functions. Xcode had the option to refactor user.authentication.do { authentication, error in to let authentication = try await user.authentication.do(), but it threw a missing parameter error (the closure it previously had).
EDIT: user.authentication.do also returns void--another reason the refactor didn't work (I think).
There is probably a much more elegant way to do all of this so excuse the possibly atrocious way I did it.
Here is the link to Google Identity Wrapper info.
Thanks in advance for your help.
Solved my own problem.
It appears (according to Apple's async/await intro video) that when you have an unsupported callback that you need to run asynchronously, you wrap it in something called a Continuation, which allows you to manually resume the function on the thread, whether throwing or returning.
So using that code allows you to run the Google Identity token refresh with async/await.
private static func auth(_ user: GIDGoogleUser) async throws -> GIDAuthentication? {
typealias AuthContinuation = CheckedContinuation<GIDAuthentication?, Error>
return try await withCheckedThrowingContinuation { (continuation: AuthContinuation) in
user.authentication.do { authentication, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: authentication)
}
}
}
}
static func search(user: GIDGoogleUser) async throws {
// some code
guard let authentication = try await auth(user) else { ... }
// some code
}
I then ran that before using Alamofire's built-in async/await functionality for each request (here's one).
let dataTask = AF.request(...).serializingDecodable(NameResponseModel.self)
let response = try await dataTask.value
return response.values[0]
I am currently building an app with an account system.
Firebase is very new to me, that's why I watched a lot of tutorials, and now its working fine.
I want to implement that the user can choose a unique username at the registration. My problem is, I really don't know how to check if this name is already taken.
I found some code for that, but that's not working, I will show you the code for the RegistrationService file.
I hope someone can explain to me how to implement this username verification. It should return an error if the username is already taken and do continue the registration if its a valid username.
Thank you!
import Combine
import Firebase
import FirebaseDatabase
import Foundation
enum RegistrationKeys: String {
case firstName
case lastname
case info
case username
}
protocol RegisterService {
func register(with details: RegistrationDetails) -> AnyPublisher<Void, Error>
}
final class RegisterServiceImpl: RegisterService {
func register(with details: RegistrationDetails) -> AnyPublisher<Void, Error> {
Deferred {
Future { promise in
Auth.auth()
.createUser(
withEmail: details.email,
password: details.password
) { res, error in
if let err = error {
promise(.failure(err))
} else {
// Success on User creation
if let uid = res?.user.uid {
let values =
[
RegistrationKeys.firstName.rawValue: details.firstName,
RegistrationKeys.lastname.rawValue: details.lastName,
RegistrationKeys.info.rawValue: details.info,
] as [String: Any]
let db = Database.database(url: "theurl")
Database.database(url: "the url")
.reference()
.child("usernames")
.child("\([RegistrationKeys.info.rawValue: details.username] as [String : Any])")
// here should be the check and then continue if its valid
db
.reference()
.child("users")
.child(uid)
.updateChildValues(values) { error, ref in
if let err = error {
promise(.failure(err))
} else {
promise(.success(()))
}
}
} else {
promise(.failure(NSError(domain: "Invalid user ID", code: 0, userInfo: nil)))
}
}
}
}
}
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}
I can see two possibilities to solve your problem:
If the e-mail can serve as the username
Firebase authentication already sends back an error message in case the e-mail (the one used when creating the user) already exists. If the e-mail passed in the following function is not unique, an error will be thrown:
Auth.auth()
.createUser(
withEmail: details.email,
password: details.password
) { res, error in
if let err = error {
promise(.failure(err))
If an additional username besides the e-mail is required
If you need usernames in addition to the e-mails, you can store them under a node "usernames", like we see in your example. Personally, I would hash them instead of storing them plain.
The structure could simply be:
{
usernames: {
username_1: true,
username_2: true,
...
username_n: true
}
}
The example below checks to see if a new username exists and stores the result in the variable isUsernameTaken:
let db = Database.database(url: "the url").reference()
let newUsername = "seeIfItIsTaken"
db.child("usernames").child(newUsername).getData() { error, snapshot in
guard error == nil else {
print("Found error \(error)")
return
}
let isUsernameTaken = snapshot.exists()
}
I want to set up a list of live list of "Move"s so I used this snippet from the amplify docs.
func createSubscription() {
subscription = Amplify.API.subscribe(request: .subscription(of: Move.self, type: .onCreate))
dataSink = subscription?.subscriptionDataPublisher.sink {
if case let .failure(apiError) = $0 {
print("Subscription has terminated with \(apiError)")
} else {
print("Subscription has been closed successfully")
}
}
receiveValue: { result in
switch result {
case .success(let createdTodo):
print("Successfully got todo from subscription: \(createdTodo)")
case .failure(let error):
print("Got failed result with \(error.errorDescription)")
}
}
}
Schema auth rules
type Move
#model
#auth( rules: [
{ allow: owner, ownerField: "owner", operations: [create, update, delete, read] },
])
{
But since I added auth to the "move" type I get this error. GraphQLResponseError<Move>: GraphQL service returned a successful response containing errors: [Amplify.GraphQLError(message: "Validation error of type MissingFieldArgument: Missing field argument owner # \'onCreateMove\'", locations: nil, path: nil, extensions: nil)]
and Recovery suggestion: The list of GraphQLError contains service-specific messages
So everything is working locally but I think I need to pass the authorization to the request but I can't find any way to do it. any Ideas how I might get this request to process properly?
got it working by writng my own request and passing the owner field directly
extension GraphQLRequest {
static func newMoves() -> GraphQLRequest<Move> {
let operationName = "getMove"
let document = """
subscription MySubscription {
onCreateMove(owner: "MyUser") {
accerationMagnitude
id
}
}
"""
return GraphQLRequest<Move>(document: document,
// variables: [],
responseType: Move.self,
decodePath: operationName)
}
}
I can't open a Realm. There is something wrong with my set up. This is the code in the app:
var configuration = user.configuration(partitionValue: "user=\(user.id)")
configuration.objectTypes = [User.self]
Realm.asyncOpen(configuration: configuration) { [weak self](userRealm, error) in
self!.setLoading(false);
guard error == nil else {
fatalError("Failed to open realm: \(error!)"). ///// here was my error before the edit (I changed the Permission Settings in Sync)
}
Error code: "Fatal error: Failed to open realm: Error Domain=io.realm.unknown Code=89 "Operation canceled" UserInfo={Category=realm.basic_system, NSLocalizedDescription=Operation canceled, Error Code=89"
Screenshot of Mongodb Sync configuration
EDIT:
Replaced the Permission settings in Sync with the ones mentioned in the Task Tracker app and that got it connected:
EDIT: the configuration settings were changed to the below as per the suggestions from Jay.
var configuration = user.configuration(partitionValue: "\(currentUser.id!)")
Here is my Scheme definition for the User Collection:
{
"properties": {
"_id": {
"bsonType": "string"
},
"_partition": {
"bsonType": "string"
},
"name": {
"bsonType": "string"
}
},
"required": [
"_id",
"_partition",
"name"
],
"title": "User"
}
This is my User class in Xcode:
class User: Object {
#objc dynamic var _id: String = ""
#objc dynamic var _partition: String = ""
#objc dynamic var name: String = ""
override static func primaryKey() -> String? {
return "_id"
}
}
EDIT: I got this going to establish a connection by updating the Sync Permissions, so the app does not crash any more. However, now I am getting this message:
Connection to daemon was invalidated
Signup successful!
Log in as user: y
Login succeeded!
2020-10-25...Sync: Connection[1]: Session[1]: client_reset_config = false, Realm exists = false, async open = false, client reset = falseSync: Connection[1]: Session[1]: client_reset_config = false, Realm exists = false, async open = false, client reset = false
2020-10-25...Sync: Connection[1]: Connected to endpoint '13.54.209.90:443' (from '.....:52139')
2020-10-25...Sync: Connection[1]: Reading failed: End of input
2020-10-25...Sync: Connection[1]: Connection closed due to error```
I don't understand that the error "("Failed to open realm: (error!)")" in my above code is not triggered, but then in the log it says that the realm does not exist! So, what's going on here?
EDIT : this is the user table in mongodb, so I created some Users successfully.
EDIT : This is the log from mongodb
As we can see the User Id and the Request ID are not the same! I guess that the two IDs should be the same string in order to be authenticated, right??
I am following the Task Tracker app tutorial from the mongodb webpage for Swift to add the user login to my app. What am I missing here?
As a complete guess, your config string is not correct
var configuration = user.configuration(partitionValue: "user=\(user.id)")
As that the partition value resolves to
partitionValue: user=Optional("5f1b586f757611faec257d88")
Try this
guard let user = your_app.currentUser() else {
print("no user")
return
}
guard let userId = user.id else {
print("no user")
return
}
var configuration = user.configuration(partitionValue: "user=\(userId)")
More to the point though, the partition value you're attempting to use is this string
user=5f1b586f757611faec257d88
and I think what you really want is use the user id
5f1b586f757611faec257d88
That's where I would start. If you're trying to leverage Realm rules, then something like _partitionKey: "team_id=1234" would work but that goes beyond the scope of the original question (and adds another layer of complexity - get it working first, then explore the rules).
In my app there is a data object named room that consists of the following properties...
class Room: NSObject {
var rid: String?
var owner: String?
var groupChatName: String?
var groupChatDescription: String?
var members: AnyObject?
}
Here is the corresponding JSON
"A632CA68-40D9-4F3A-B8C8-245457057443" : {
"groupChatDescription" : "Test Description",
"groupChatName" : "Test Name",
"members" : {
"WuqCAt4mM3h0P0X1m7hVZ7NQyLC2" : {
"username" : "Steve"
}
},
"owner" : "Steve"
},
When I retrieve these values from my database, I put them in a dictionary
func fetchAllRooms(){
Database.database().reference().child("rooms").observe(.childAdded, with: {(snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let room = Room()
room.rid = snapshot.key
room.setValuesForKeys(dictionary)
self.rooms.append(room)
print("\(room.members)")
DispatchQueue.main.async(execute: {
self.collectionView?.reloadData()
})
}
print("end of room snap")
}, withCancel: nil)
}
The data matches up correctly however handling the members is proving more difficult than I originally expected.
print("\(room.members)") Returns
Optional({
WuqCAt4mM3h0P0X1m7hVZ7NQyLC2 = {
username = "Steve";
};
})
That is a lot of extraneous text. Especially considering that this is only one member and I need to return a list of members in app.
What I believe would be best for my case is to create a dictionary for the members within the current dictionary. considering each member will have additional attributes such as a reference to their profile image.
So I could call something like print("\(room.members.usernames)") and get all the usernames and still be able to use the other attributes later on.
But I don't know if this is the best solution or even where to begin. I was hoping FireBase would have some built in methods to handle this but so far I haven't been able to find anything.
Any answers, suggestions, and or references are greatly appreciated.