Skip to content

Commit

Permalink
feat: add create(), replace(), and update() to ParseObjects (#299)
Browse files Browse the repository at this point in the history
* feat: add create() and update() methods to ParseObjects

* Change WriteResponse to BatchResponse

* Add replace() and replaceAll()

* Fix failing tests

* Fix Swift 5.2 builds
  • Loading branch information
cbaker6 authored Dec 12, 2021
1 parent aa22991 commit ebaaf12
Show file tree
Hide file tree
Showing 47 changed files with 4,469 additions and 646 deletions.
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

0 comments on commit ebaaf12

Please sign in to comment.