Skip to content

feat: add create(), replace(), and update() to ParseObjects #299

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 5 commits into from
Dec 12, 2021
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
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@

### main

[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.4.0...main)
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.5.0...main)
* _Contributing to this repo? Add info about your change here to be included in the next release_

### 2.5.0
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.4.0...2.5.0)

__Improvements__
- Added create(), replace(), update(), createAll(), replaceAll(), and updateAll() to ParseObjects. Currently, update() and updateAll() are unavaivalble due to limitations of PATCH on the Parse Server ([#299](https://github.com/parse-community/Parse-Swift/pull/299)), thanks to [Corey Baker](https://github.com/cbaker6).
- Added convenience methods to convert ParseObject's to Pointer<ParseObject>'s for QueryConstraint's: !=, containedIn, notContainedIn, containedBy, containsAll ([#298](https://github.com/parse-community/Parse-Swift/pull/298)), thanks to [Corey Baker](https://github.com/cbaker6).

### 2.4.0
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.3.1...2.4.0)

__Improvements__
- Added additional methods to ParseRelation to make it easier to create and query relations ([#294](https://github.com/parse-community/Parse-Swift/pull/294)), thanks to [Corey Baker](https://github.com/cbaker6).
- Enable async/await for iOS13, tvOS13, watchOS6, and macOS10_15. All async/await methods are @MainActor's. Requires Xcode 13.2 or above to use async/await. Not compatible with Xcode 13.0/1, will need to upgrade to 13.2+. Still works with Xcode 11/12 ([#278](https://github.com/parse-community/Parse-Swift/pull/278)), thanks to [Corey Baker](https://github.com/cbaker6).
- Enable async/await for iOS13, tvOS13, watchOS6, and macOS10_15. All async/await methods are MainActor's. Requires Xcode 13.2 or above to use async/await. Not compatible with Xcode 13.0/1, will need to upgrade to 13.2+. Still works with Xcode 11/12 ([#278](https://github.com/parse-community/Parse-Swift/pull/278)), thanks to [Corey Baker](https://github.com/cbaker6).

__Fixes__
- When transactions are enabled errors are now thrown from the client if the amount of objects in a transaction exceeds the batch size. An error will also be thrown if a developer attempts to save objects in a transation that has unsaved children ([#295](https://github.com/parse-community/Parse-Swift/pull/294)), thanks to [Corey Baker](https://github.com/cbaker6).
Expand Down
46 changes: 37 additions & 9 deletions Sources/ParseSwift/API/API+Command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ internal extension API {
}
}
} else {
//ParseFiles are handled with a dedicated URLSession
// ParseFiles are handled with a dedicated URLSession
if method == .POST || method == .PUT || method == .PATCH {
switch self.prepareURLRequest(options: options,
childObjects: childObjects,
Expand Down Expand Up @@ -262,7 +262,7 @@ internal extension API {
childFiles: [UUID: ParseFile]? = nil) -> Result<URLRequest, ParseError> {
let params = self.params?.getQueryItems()
var headers = API.getHeaders(options: options)
if !(method == .POST) && !(method == .PUT) && !(method == .PATCH) {
if method == .GET || method == .DELETE {
headers.removeValue(forKey: "X-Parse-Request-Id")
}
let url = parseURL == nil ?
Expand Down Expand Up @@ -390,37 +390,55 @@ internal extension API.Command {
throw ParseError(code: .missingObjectId, message: "objectId must not be nil")
}
if object.isSaved {
return update(object)
return try replace(object) // Should be switched to "update" when server supports PATCH.
}
return create(object)
}

// MARK: Saving ParseObjects - private
private static func create<T>(_ object: T) -> API.Command<T, T> where T: ParseObject {
static func create<T>(_ object: T) -> API.Command<T, T> where T: ParseObject {
var object = object
if object.ACL == nil,
let acl = try? ParseACL.defaultACL() {
object.ACL = acl
}
let mapper = { (data) -> T in
try ParseCoding.jsonDecoder().decode(SaveResponse.self, from: data).apply(to: object)
try ParseCoding.jsonDecoder().decode(CreateResponse.self, from: data).apply(to: object)
}
return API.Command<T, T>(method: .POST,
path: object.endpoint(.POST),
body: object,
mapper: mapper)
}

private static func update<T>(_ object: T) -> API.Command<T, T> where T: ParseObject {
static func replace<T>(_ object: T) throws -> API.Command<T, T> where T: ParseObject {
guard object.objectId != nil else {
throw ParseError(code: .missingObjectId,
message: "objectId must not be nil")
}
let mapper = { (data) -> T in
try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: object)
try ParseCoding.jsonDecoder().decode(ReplaceResponse.self, from: data).apply(to: object)
}
return API.Command<T, T>(method: .PUT,
path: object.endpoint,
body: object,
mapper: mapper)
}

static func update<T>(_ object: T) throws -> API.Command<T, T> where T: ParseObject {
guard object.objectId != nil else {
throw ParseError(code: .missingObjectId,
message: "objectId must not be nil")
}
let mapper = { (data) -> T in
try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: object)
}
return API.Command<T, T>(method: .PATCH,
path: object.endpoint,
body: object,
mapper: mapper)
}

// MARK: Fetching
static func fetch<T>(_ object: T, include: [String]?) throws -> API.Command<T, T> where T: ParseObject {
guard object.objectId != nil else {
Expand Down Expand Up @@ -458,14 +476,24 @@ internal extension API.Command where T: ParseObject {

let mapper = { (data: Data) -> [Result<T, ParseError>] in

let decodingType = [BatchResponseItem<WriteResponse>].self
let decodingType = [BatchResponseItem<BatchResponse>].self
do {
let responses = try ParseCoding.jsonDecoder().decode(decodingType, from: data)
return commands.enumerated().map({ (object) -> (Result<T, ParseError>) in
let response = responses[object.offset]
if let success = response.success,
let body = object.element.body {
return .success(success.apply(to: body, method: object.element.method))
do {
let updatedObject = try success.apply(to: body,
method: object.element.method)
return .success(updatedObject)
} catch {
guard let parseError = error as? ParseError else {
return .failure(ParseError(code: .unknownError,
message: error.localizedDescription))
}
return .failure(parseError)
}
} else {
guard let parseError = response.error else {
return .failure(ParseError(code: .unknownError, message: "unknown error"))
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/API/API+NonParseBodyCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ internal extension API {
// MARK: URL Preperation
func prepareURLRequest(options: API.Options) -> Result<URLRequest, ParseError> {
var headers = API.getHeaders(options: options)
if !(method == .POST) && !(method == .PUT) && !(method == .PATCH) {
if method == .GET || method == .DELETE {
headers.removeValue(forKey: "X-Parse-Request-Id")
}
let url = ParseSwift.configuration.serverURL.appendingPathComponent(path.urlComponent)
Expand Down
63 changes: 48 additions & 15 deletions Sources/ParseSwift/API/Responses.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import Foundation

internal struct SaveResponse: Decodable {
internal struct CreateResponse: Decodable {
var objectId: String
var createdAt: Date
var updatedAt: Date {
Expand All @@ -24,9 +24,25 @@ internal struct SaveResponse: Decodable {
}
}

internal struct UpdateSessionTokenResponse: Decodable {
var updatedAt: Date
let sessionToken: String?
internal struct ReplaceResponse: Decodable {
var createdAt: Date?
var updatedAt: Date?

func apply<T>(to object: T) throws -> T where T: ParseObject {
guard let objectId = object.objectId else {
throw ParseError(code: .missingObjectId,
message: "Response from server should not have an objectId of nil")
}
guard let createdAt = createdAt else {
guard let updatedAt = updatedAt else {
throw ParseError(code: .unknownError,
message: "Response from server should not have an updatedAt of nil")
}
return UpdateResponse(updatedAt: updatedAt).apply(to: object)
}
return CreateResponse(objectId: objectId,
createdAt: createdAt).apply(to: object)
}
}

internal struct UpdateResponse: Decodable {
Expand All @@ -39,37 +55,54 @@ internal struct UpdateResponse: Decodable {
}
}

internal struct UpdateSessionTokenResponse: Decodable {
var updatedAt: Date
let sessionToken: String?
}

// MARK: ParseObject Batch
internal struct BatchResponseItem<T>: Codable where T: Codable {
let success: T?
let error: ParseError?
}

internal struct WriteResponse: Codable {
internal struct BatchResponse: Codable {
var objectId: String?
var createdAt: Date?
var updatedAt: Date?

func asSaveResponse() -> SaveResponse {
guard let objectId = objectId, let createdAt = createdAt else {
fatalError("Cannot create a SaveResponse without objectId")
func asCreateResponse() throws -> CreateResponse {
guard let objectId = objectId else {
throw ParseError(code: .missingObjectId,
message: "Response from server should not have an objectId of nil")
}
guard let createdAt = createdAt else {
throw ParseError(code: .unknownError,
message: "Response from server should not have an createdAt of nil")
}
return SaveResponse(objectId: objectId, createdAt: createdAt)
return CreateResponse(objectId: objectId, createdAt: createdAt)
}

func asReplaceResponse() -> ReplaceResponse {
ReplaceResponse(createdAt: createdAt, updatedAt: updatedAt)
}

func asUpdateResponse() -> UpdateResponse {
func asUpdateResponse() throws -> UpdateResponse {
guard let updatedAt = updatedAt else {
fatalError("Cannot create an UpdateResponse without updatedAt")
throw ParseError(code: .unknownError,
message: "Response from server should not have an updatedAt of nil")
}
return UpdateResponse(updatedAt: updatedAt)
}

func apply<T>(to object: T, method: API.Method) -> T where T: ParseObject {
func apply<T>(to object: T, method: API.Method) throws -> T where T: ParseObject {
switch method {
case .POST:
return asSaveResponse().apply(to: object)
case .PUT, .PATCH:
return asUpdateResponse().apply(to: object)
return try asCreateResponse().apply(to: object)
case .PUT:
return try asReplaceResponse().apply(to: object)
case .PATCH:
return try asUpdateResponse().apply(to: object)
case .GET:
fatalError("Parse-server doesn't support batch fetching like this. Try \"fetchAll\".")
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
#if swift(>=5.5) && canImport(_Concurrency)
import Foundation

@MainActor
public extension ParseApple {
// MARK: Async/Await

Expand All @@ -19,7 +18,7 @@ public extension ParseApple {
- parameter identityToken: The `identityToken` from `ASAuthorizationAppleIDCredential`.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`..
- throws: An error of type `ParseError`.
*/
func login(user: String,
identityToken: Data,
Expand All @@ -37,7 +36,7 @@ public extension ParseApple {
- parameter authData: Dictionary containing key/values.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`..
- throws: An error of type `ParseError`.
*/
func login(authData: [String: String],
options: API.Options = []) async throws -> AuthenticatedUser {
Expand All @@ -49,7 +48,6 @@ public extension ParseApple {
}
}

@MainActor
public extension ParseApple {

/**
Expand All @@ -58,7 +56,7 @@ public extension ParseApple {
- parameter identityToken: The `identityToken` from `ASAuthorizationAppleIDCredential`.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`..
- throws: An error of type `ParseError`.
*/
func link(user: String,
identityToken: Data,
Expand All @@ -76,7 +74,7 @@ public extension ParseApple {
- parameter authData: Dictionary containing key/values.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`..
- throws: An error of type `ParseError`.
*/
func link(authData: [String: String],
options: API.Options = []) async throws -> AuthenticatedUser {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
#if swift(>=5.5) && canImport(_Concurrency)
import Foundation

@MainActor
public extension ParseFacebook {
// MARK: Async/Await

Expand All @@ -20,7 +19,7 @@ public extension ParseFacebook {
- parameter expiresIn: Optional expiration in seconds for Facebook login.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`..
- throws: An error of type `ParseError`.
*/
func login(userId: String,
authenticationToken: String,
Expand All @@ -42,7 +41,7 @@ public extension ParseFacebook {
- parameter expiresIn: Optional expiration in seconds for Facebook login.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`..
- throws: An error of type `ParseError`.
*/
func login(userId: String,
accessToken: String,
Expand All @@ -61,7 +60,7 @@ public extension ParseFacebook {
Login a `ParseUser` *asynchronously* using Facebook authentication for graph API login.
- parameter authData: Dictionary containing key/values.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`..
- throws: An error of type `ParseError`.
*/
func login(authData: [String: String],
options: API.Options = []) async throws -> AuthenticatedUser {
Expand All @@ -73,7 +72,6 @@ public extension ParseFacebook {
}
}

@MainActor
public extension ParseFacebook {
/**
Link the *current* `ParseUser` *asynchronously* using Facebook authentication for limited login.
Expand All @@ -82,7 +80,7 @@ public extension ParseFacebook {
- parameter expiresIn: Optional expiration in seconds for Facebook login.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`..
- throws: An error of type `ParseError`.
*/
func link(userId: String,
authenticationToken: String,
Expand All @@ -104,7 +102,7 @@ public extension ParseFacebook {
- parameter expiresIn: Optional expiration in seconds for Facebook login.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`..
- throws: An error of type `ParseError`.
*/
func link(userId: String,
accessToken: String,
Expand All @@ -124,7 +122,7 @@ public extension ParseFacebook {
- parameter authData: Dictionary containing key/values.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`..
- throws: An error of type `ParseError`.
*/
func link(authData: [String: String],
options: API.Options = []) async throws -> AuthenticatedUser {
Expand Down
Loading