Skip to content

Commit ebaaf12

Browse files
authored
feat: add create(), replace(), and update() to ParseObjects (#299)
* feat: add create() and update() methods to ParseObjects * Change WriteResponse to BatchResponse * Add replace() and replaceAll() * Fix failing tests * Fix Swift 5.2 builds
1 parent aa22991 commit ebaaf12

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+4469
-646
lines changed

CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@
22

33
### main
44

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

8+
### 2.5.0
9+
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.4.0...2.5.0)
10+
11+
__Improvements__
12+
- 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).
13+
- 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).
14+
815
### 2.4.0
916
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.3.1...2.4.0)
1017

1118
__Improvements__
1219
- 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).
13-
- 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).
20+
- 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).
1421

1522
__Fixes__
1623
- 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).

Sources/ParseSwift/API/API+Command.swift

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ internal extension API {
145145
}
146146
}
147147
} else {
148-
//ParseFiles are handled with a dedicated URLSession
148+
// ParseFiles are handled with a dedicated URLSession
149149
if method == .POST || method == .PUT || method == .PATCH {
150150
switch self.prepareURLRequest(options: options,
151151
childObjects: childObjects,
@@ -262,7 +262,7 @@ internal extension API {
262262
childFiles: [UUID: ParseFile]? = nil) -> Result<URLRequest, ParseError> {
263263
let params = self.params?.getQueryItems()
264264
var headers = API.getHeaders(options: options)
265-
if !(method == .POST) && !(method == .PUT) && !(method == .PATCH) {
265+
if method == .GET || method == .DELETE {
266266
headers.removeValue(forKey: "X-Parse-Request-Id")
267267
}
268268
let url = parseURL == nil ?
@@ -390,37 +390,55 @@ internal extension API.Command {
390390
throw ParseError(code: .missingObjectId, message: "objectId must not be nil")
391391
}
392392
if object.isSaved {
393-
return update(object)
393+
return try replace(object) // Should be switched to "update" when server supports PATCH.
394394
}
395395
return create(object)
396396
}
397397

398398
// MARK: Saving ParseObjects - private
399-
private static func create<T>(_ object: T) -> API.Command<T, T> where T: ParseObject {
399+
static func create<T>(_ object: T) -> API.Command<T, T> where T: ParseObject {
400400
var object = object
401401
if object.ACL == nil,
402402
let acl = try? ParseACL.defaultACL() {
403403
object.ACL = acl
404404
}
405405
let mapper = { (data) -> T in
406-
try ParseCoding.jsonDecoder().decode(SaveResponse.self, from: data).apply(to: object)
406+
try ParseCoding.jsonDecoder().decode(CreateResponse.self, from: data).apply(to: object)
407407
}
408408
return API.Command<T, T>(method: .POST,
409409
path: object.endpoint(.POST),
410410
body: object,
411411
mapper: mapper)
412412
}
413413

414-
private static func update<T>(_ object: T) -> API.Command<T, T> where T: ParseObject {
414+
static func replace<T>(_ object: T) throws -> API.Command<T, T> where T: ParseObject {
415+
guard object.objectId != nil else {
416+
throw ParseError(code: .missingObjectId,
417+
message: "objectId must not be nil")
418+
}
415419
let mapper = { (data) -> T in
416-
try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: object)
420+
try ParseCoding.jsonDecoder().decode(ReplaceResponse.self, from: data).apply(to: object)
417421
}
418422
return API.Command<T, T>(method: .PUT,
419423
path: object.endpoint,
420424
body: object,
421425
mapper: mapper)
422426
}
423427

428+
static func update<T>(_ object: T) throws -> API.Command<T, T> where T: ParseObject {
429+
guard object.objectId != nil else {
430+
throw ParseError(code: .missingObjectId,
431+
message: "objectId must not be nil")
432+
}
433+
let mapper = { (data) -> T in
434+
try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: object)
435+
}
436+
return API.Command<T, T>(method: .PATCH,
437+
path: object.endpoint,
438+
body: object,
439+
mapper: mapper)
440+
}
441+
424442
// MARK: Fetching
425443
static func fetch<T>(_ object: T, include: [String]?) throws -> API.Command<T, T> where T: ParseObject {
426444
guard object.objectId != nil else {
@@ -458,14 +476,24 @@ internal extension API.Command where T: ParseObject {
458476

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

461-
let decodingType = [BatchResponseItem<WriteResponse>].self
479+
let decodingType = [BatchResponseItem<BatchResponse>].self
462480
do {
463481
let responses = try ParseCoding.jsonDecoder().decode(decodingType, from: data)
464482
return commands.enumerated().map({ (object) -> (Result<T, ParseError>) in
465483
let response = responses[object.offset]
466484
if let success = response.success,
467485
let body = object.element.body {
468-
return .success(success.apply(to: body, method: object.element.method))
486+
do {
487+
let updatedObject = try success.apply(to: body,
488+
method: object.element.method)
489+
return .success(updatedObject)
490+
} catch {
491+
guard let parseError = error as? ParseError else {
492+
return .failure(ParseError(code: .unknownError,
493+
message: error.localizedDescription))
494+
}
495+
return .failure(parseError)
496+
}
469497
} else {
470498
guard let parseError = response.error else {
471499
return .failure(ParseError(code: .unknownError, message: "unknown error"))

Sources/ParseSwift/API/API+NonParseBodyCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ internal extension API {
8484
// MARK: URL Preperation
8585
func prepareURLRequest(options: API.Options) -> Result<URLRequest, ParseError> {
8686
var headers = API.getHeaders(options: options)
87-
if !(method == .POST) && !(method == .PUT) && !(method == .PATCH) {
87+
if method == .GET || method == .DELETE {
8888
headers.removeValue(forKey: "X-Parse-Request-Id")
8989
}
9090
let url = ParseSwift.configuration.serverURL.appendingPathComponent(path.urlComponent)

Sources/ParseSwift/API/Responses.swift

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import Foundation
1010

11-
internal struct SaveResponse: Decodable {
11+
internal struct CreateResponse: Decodable {
1212
var objectId: String
1313
var createdAt: Date
1414
var updatedAt: Date {
@@ -24,9 +24,25 @@ internal struct SaveResponse: Decodable {
2424
}
2525
}
2626

27-
internal struct UpdateSessionTokenResponse: Decodable {
28-
var updatedAt: Date
29-
let sessionToken: String?
27+
internal struct ReplaceResponse: Decodable {
28+
var createdAt: Date?
29+
var updatedAt: Date?
30+
31+
func apply<T>(to object: T) throws -> T where T: ParseObject {
32+
guard let objectId = object.objectId else {
33+
throw ParseError(code: .missingObjectId,
34+
message: "Response from server should not have an objectId of nil")
35+
}
36+
guard let createdAt = createdAt else {
37+
guard let updatedAt = updatedAt else {
38+
throw ParseError(code: .unknownError,
39+
message: "Response from server should not have an updatedAt of nil")
40+
}
41+
return UpdateResponse(updatedAt: updatedAt).apply(to: object)
42+
}
43+
return CreateResponse(objectId: objectId,
44+
createdAt: createdAt).apply(to: object)
45+
}
3046
}
3147

3248
internal struct UpdateResponse: Decodable {
@@ -39,37 +55,54 @@ internal struct UpdateResponse: Decodable {
3955
}
4056
}
4157

58+
internal struct UpdateSessionTokenResponse: Decodable {
59+
var updatedAt: Date
60+
let sessionToken: String?
61+
}
62+
4263
// MARK: ParseObject Batch
4364
internal struct BatchResponseItem<T>: Codable where T: Codable {
4465
let success: T?
4566
let error: ParseError?
4667
}
4768

48-
internal struct WriteResponse: Codable {
69+
internal struct BatchResponse: Codable {
4970
var objectId: String?
5071
var createdAt: Date?
5172
var updatedAt: Date?
5273

53-
func asSaveResponse() -> SaveResponse {
54-
guard let objectId = objectId, let createdAt = createdAt else {
55-
fatalError("Cannot create a SaveResponse without objectId")
74+
func asCreateResponse() throws -> CreateResponse {
75+
guard let objectId = objectId else {
76+
throw ParseError(code: .missingObjectId,
77+
message: "Response from server should not have an objectId of nil")
78+
}
79+
guard let createdAt = createdAt else {
80+
throw ParseError(code: .unknownError,
81+
message: "Response from server should not have an createdAt of nil")
5682
}
57-
return SaveResponse(objectId: objectId, createdAt: createdAt)
83+
return CreateResponse(objectId: objectId, createdAt: createdAt)
84+
}
85+
86+
func asReplaceResponse() -> ReplaceResponse {
87+
ReplaceResponse(createdAt: createdAt, updatedAt: updatedAt)
5888
}
5989

60-
func asUpdateResponse() -> UpdateResponse {
90+
func asUpdateResponse() throws -> UpdateResponse {
6191
guard let updatedAt = updatedAt else {
62-
fatalError("Cannot create an UpdateResponse without updatedAt")
92+
throw ParseError(code: .unknownError,
93+
message: "Response from server should not have an updatedAt of nil")
6394
}
6495
return UpdateResponse(updatedAt: updatedAt)
6596
}
6697

67-
func apply<T>(to object: T, method: API.Method) -> T where T: ParseObject {
98+
func apply<T>(to object: T, method: API.Method) throws -> T where T: ParseObject {
6899
switch method {
69100
case .POST:
70-
return asSaveResponse().apply(to: object)
71-
case .PUT, .PATCH:
72-
return asUpdateResponse().apply(to: object)
101+
return try asCreateResponse().apply(to: object)
102+
case .PUT:
103+
return try asReplaceResponse().apply(to: object)
104+
case .PATCH:
105+
return try asUpdateResponse().apply(to: object)
73106
case .GET:
74107
fatalError("Parse-server doesn't support batch fetching like this. Try \"fetchAll\".")
75108
default:

Sources/ParseSwift/Authentication/3rd Party/ParseApple/ParseApple+async.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
#if swift(>=5.5) && canImport(_Concurrency)
1010
import Foundation
1111

12-
@MainActor
1312
public extension ParseApple {
1413
// MARK: Async/Await
1514

@@ -19,7 +18,7 @@ public extension ParseApple {
1918
- parameter identityToken: The `identityToken` from `ASAuthorizationAppleIDCredential`.
2019
- parameter options: A set of header options sent to the server. Defaults to an empty set.
2120
- returns: An instance of the logged in `ParseUser`.
22-
- throws: An error of type `ParseError`..
21+
- throws: An error of type `ParseError`.
2322
*/
2423
func login(user: String,
2524
identityToken: Data,
@@ -37,7 +36,7 @@ public extension ParseApple {
3736
- parameter authData: Dictionary containing key/values.
3837
- parameter options: A set of header options sent to the server. Defaults to an empty set.
3938
- returns: An instance of the logged in `ParseUser`.
40-
- throws: An error of type `ParseError`..
39+
- throws: An error of type `ParseError`.
4140
*/
4241
func login(authData: [String: String],
4342
options: API.Options = []) async throws -> AuthenticatedUser {
@@ -49,7 +48,6 @@ public extension ParseApple {
4948
}
5049
}
5150

52-
@MainActor
5351
public extension ParseApple {
5452

5553
/**
@@ -58,7 +56,7 @@ public extension ParseApple {
5856
- parameter identityToken: The `identityToken` from `ASAuthorizationAppleIDCredential`.
5957
- parameter options: A set of header options sent to the server. Defaults to an empty set.
6058
- returns: An instance of the logged in `ParseUser`.
61-
- throws: An error of type `ParseError`..
59+
- throws: An error of type `ParseError`.
6260
*/
6361
func link(user: String,
6462
identityToken: Data,
@@ -76,7 +74,7 @@ public extension ParseApple {
7674
- parameter authData: Dictionary containing key/values.
7775
- parameter options: A set of header options sent to the server. Defaults to an empty set.
7876
- returns: An instance of the logged in `ParseUser`.
79-
- throws: An error of type `ParseError`..
77+
- throws: An error of type `ParseError`.
8078
*/
8179
func link(authData: [String: String],
8280
options: API.Options = []) async throws -> AuthenticatedUser {

Sources/ParseSwift/Authentication/3rd Party/ParseFacebook/ParseFacebook+async.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
#if swift(>=5.5) && canImport(_Concurrency)
1010
import Foundation
1111

12-
@MainActor
1312
public extension ParseFacebook {
1413
// MARK: Async/Await
1514

@@ -20,7 +19,7 @@ public extension ParseFacebook {
2019
- parameter expiresIn: Optional expiration in seconds for Facebook login.
2120
- parameter options: A set of header options sent to the server. Defaults to an empty set.
2221
- returns: An instance of the logged in `ParseUser`.
23-
- throws: An error of type `ParseError`..
22+
- throws: An error of type `ParseError`.
2423
*/
2524
func login(userId: String,
2625
authenticationToken: String,
@@ -42,7 +41,7 @@ public extension ParseFacebook {
4241
- parameter expiresIn: Optional expiration in seconds for Facebook login.
4342
- parameter options: A set of header options sent to the server. Defaults to an empty set.
4443
- returns: An instance of the logged in `ParseUser`.
45-
- throws: An error of type `ParseError`..
44+
- throws: An error of type `ParseError`.
4645
*/
4746
func login(userId: String,
4847
accessToken: String,
@@ -61,7 +60,7 @@ public extension ParseFacebook {
6160
Login a `ParseUser` *asynchronously* using Facebook authentication for graph API login.
6261
- parameter authData: Dictionary containing key/values.
6362
- returns: An instance of the logged in `ParseUser`.
64-
- throws: An error of type `ParseError`..
63+
- throws: An error of type `ParseError`.
6564
*/
6665
func login(authData: [String: String],
6766
options: API.Options = []) async throws -> AuthenticatedUser {
@@ -73,7 +72,6 @@ public extension ParseFacebook {
7372
}
7473
}
7574

76-
@MainActor
7775
public extension ParseFacebook {
7876
/**
7977
Link the *current* `ParseUser` *asynchronously* using Facebook authentication for limited login.
@@ -82,7 +80,7 @@ public extension ParseFacebook {
8280
- parameter expiresIn: Optional expiration in seconds for Facebook login.
8381
- parameter options: A set of header options sent to the server. Defaults to an empty set.
8482
- returns: An instance of the logged in `ParseUser`.
85-
- throws: An error of type `ParseError`..
83+
- throws: An error of type `ParseError`.
8684
*/
8785
func link(userId: String,
8886
authenticationToken: String,
@@ -104,7 +102,7 @@ public extension ParseFacebook {
104102
- parameter expiresIn: Optional expiration in seconds for Facebook login.
105103
- parameter options: A set of header options sent to the server. Defaults to an empty set.
106104
- returns: An instance of the logged in `ParseUser`.
107-
- throws: An error of type `ParseError`..
105+
- throws: An error of type `ParseError`.
108106
*/
109107
func link(userId: String,
110108
accessToken: String,
@@ -124,7 +122,7 @@ public extension ParseFacebook {
124122
- parameter authData: Dictionary containing key/values.
125123
- parameter options: A set of header options sent to the server. Defaults to an empty set.
126124
- returns: An instance of the logged in `ParseUser`.
127-
- throws: An error of type `ParseError`..
125+
- throws: An error of type `ParseError`.
128126
*/
129127
func link(authData: [String: String],
130128
options: API.Options = []) async throws -> AuthenticatedUser {

0 commit comments

Comments
 (0)