Skip to content

feat: Add isNull QueryConstraint and set/foreset null operations #308

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
Jan 3, 2022
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
12 changes: 8 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@
### 3.0.0
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.5.1...3.0.0)

__Improvements__
- (Breaking Change) Adds options to matchesText query constraint along with the ability to see matching score. The compiler should recommend the new score property to all ParseObjects ([#306](https://github.com/parse-community/Parse-Swift/pull/306)), thanks to [Corey Baker](https://github.com/cbaker6).
__New features__
- (Breaking Change) Adds options to matchesText QueryConstraint along with the ability to see matching score. The compiler should recommend the new score property to all ParseObjects ([#306](https://github.com/parse-community/Parse-Swift/pull/306)), thanks to [Corey Baker](https://github.com/cbaker6).
- Adds withCount query ([#306](https://github.com/parse-community/Parse-Swift/pull/306)), thanks to [Corey Baker](https://github.com/cbaker6).
- Adds auth support for GitHub, Google, and LinkedIn ([#307](https://github.com/parse-community/Parse-Swift/pull/307)), thanks to [Corey Baker](https://github.com/cbaker6).
- Adds isNull QueryConstraint along with the ability set/forceSet null using ParseOperation ([#308](https://github.com/parse-community/Parse-Swift/pull/308)), thanks to [Corey Baker](https://github.com/cbaker6).

__Improvements__
- Improve QueryWhere by making at a set of QueryConstraint's instead of any array. This dedupes the same constraint when encoding the query; improving the encoding speed when the same constraints are added ([#308](https://github.com/parse-community/Parse-Swift/pull/308)), thanks to [Corey Baker](https://github.com/cbaker6).

### 2.5.1
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.5.0...2.5.1)
Expand All @@ -26,14 +30,14 @@ __Fixes__
### 2.5.0
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.4.0...2.5.0)

__Improvements__
__New features__
- 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__
__New features__
- 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).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct GameScore: ParseObject {

//: Your own properties.
var points: Int? = 0
var name: String?
}

//: It's recommended to place custom initializers in an extension
Expand All @@ -43,7 +44,8 @@ extension GameScore {
//: First lets create another GameScore.
let savedScore: GameScore!
do {
savedScore = try GameScore(points: 102).save()
let score = GameScore(points: 102, name: "player1")
savedScore = try score.save()
} catch {
savedScore = nil
assertionFailure("Error saving: \(error)")
Expand All @@ -70,6 +72,22 @@ do {
print(error)
}

//: Query all scores whose is null or undefined.
let query1 = GameScore.query(notNull(key: "name"))
let results1 = try query1.find()
print("Total found: \(results1.count)")
results1.forEach { score in
print("Found score with a name: \(score)")
}

//: Query all scores whose name is undefined.
let query2 = GameScore.query(exists(key: "name"))
let results2 = try query2.find()
print("Total found: \(results2.count)")
results2.forEach { score in
print("Found score with a name: \(score)")
}

//: You can also remove a value for a property using unset.
let unsetOperation = savedScore
.operation.unset(("points", \.points))
Expand All @@ -80,6 +98,33 @@ do {
print(error)
}

//: There may be cases where you want to set/forceSet a value to null
//: instead of unsetting
let setToNullOperation = savedScore
.operation.set(("name", \.name), value: nil)
do {
let updatedScore = try setToNullOperation.save()
print("Updated score: \(updatedScore). Check the new score on Parse Dashboard.")
} catch {
print(error)
}

//: Query all scores whose name is null or undefined.
let query3 = GameScore.query(isNull(key: "name"))
let results3 = try query3.find()
print("Total found: \(results3.count)")
results3.forEach { score in
print("Found score with name is null: \(score)")
}

//: Query all scores whose name is undefined.
let query4 = GameScore.query(doesNotExist(key: "name"))
let results4 = try query4.find()
print("Total found: \(results4.count)")
results4.forEach { score in
print("Found score with name does not exist: \(score)")
}

//: There are other operations: set/forceSet/unset/add/remove, etc. objects from `ParseObject`s.
//: In fact, the `users` and `roles` relations from `ParseRoles` used the add/remove operations.
//: Multiple operations can be chained together. See:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ query2.find { results in
}
}

//: If you want to query for points > 50 and don't have a `ParseGeoPoint`.
//: If you want to query for points > 50 and whose location is undefined.
var query3 = GameScore.query("points" > 50, doesNotExist(key: "location"))
query3.find { results in
switch results {
Expand All @@ -154,7 +154,25 @@ query3.find { results in
}
}

//: If you want to query for points > 9 and have a `ParseGeoPoint`.
//: If you want to query for points > 50 and whose location is null or undefined.
var anotherQuery3 = GameScore.query("points" > 50, isNull(key: "location"))
anotherQuery3.find { results in
switch results {
case .success(let scores):

scores.forEach { (score) in
print("""
Someone has a points value of \"\(String(describing: score.points))\"
with no geopoint \(String(describing: score.location))
""")
}

case .failure(let error):
assertionFailure("Error querying: \(error)")
}
}

//: If you want to query for points > 9 and whose location is not undefined.
var query4 = GameScore.query("points" > 9, exists(key: "location"))
query4.find { results in
switch results {
Expand All @@ -173,6 +191,25 @@ query4.find { results in
}
}

//: Get the same results as the previous query whose location is not null or undefined.
var anotherQuery4 = GameScore.query("points" > 9, notNull(key: "location"))
anotherQuery4.find { results in
switch results {
case .success(let scores):

assert(scores.count >= 1)
scores.forEach { (score) in
print("""
Someone has a points of \"\(String(describing: score.points))\"
with geopoint \(String(describing: score.location))
""")
}

case .failure(let error):
assertionFailure("Error querying: \(error)")
}
}

let query5 = GameScore.query("points" == 50)
let query6 = GameScore.query("points" == 200)

Expand Down
39 changes: 27 additions & 12 deletions Sources/ParseSwift/Types/ParseOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public struct ParseOperation<T>: Savable where T: ParseObject {

var target: T
var operations = [String: Encodable]()
var keysToNull = Set<String>()

public init(target: T) {
self.target = target
Expand All @@ -31,11 +32,15 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
- key: A tuple consisting of the key and the respective KeyPath of the object.
- value: The value to set it to.
- returns: The updated operations.
- Note: Set the value to "nil" if you want it to be "null" on the Parse Server.
*/
public func set<W>(_ key: (String, WritableKeyPath<T, W>),
value: W) throws -> Self where W: Encodable {
public func set<W>(_ key: (String, WritableKeyPath<T, W?>),
value: W?) -> Self where W: Encodable {
var mutableOperation = self
if !target[keyPath: key.1].isEqual(value) {
if value == nil && target[keyPath: key.1] != nil {
mutableOperation.keysToNull.insert(key.0)
mutableOperation.target[keyPath: key.1] = value
} else if !target[keyPath: key.1].isEqual(value) {
mutableOperation.operations[key.0] = value
mutableOperation.target[keyPath: key.1] = value
}
Expand All @@ -48,11 +53,16 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
- key: A tuple consisting of the key and the respective KeyPath of the object.
- value: The value to set it to.
- returns: The updated operations.
- Note: Set the value to "nil" if you want it to be "null" on the Parse Server.
*/
public func forceSet<W>(_ key: (String, WritableKeyPath<T, W>),
value: W) throws -> Self where W: Encodable {
public func forceSet<W>(_ key: (String, WritableKeyPath<T, W?>),
value: W?) -> Self where W: Encodable {
var mutableOperation = self
mutableOperation.operations[key.0] = value
if value != nil {
mutableOperation.operations[key.0] = value
} else {
mutableOperation.keysToNull.insert(key.0)
}
mutableOperation.target[keyPath: key.1] = value
return mutableOperation
}
Expand Down Expand Up @@ -93,7 +103,7 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
- returns: The updated operations.
*/
public func addUnique<V>(_ key: (String, WritableKeyPath<T, [V]>),
objects: [V]) throws -> Self where V: Encodable, V: Hashable {
objects: [V]) -> Self where V: Encodable, V: Hashable {
var mutableOperation = self
mutableOperation.operations[key.0] = AddUnique(objects: objects)
var values = target[keyPath: key.1]
Expand All @@ -111,7 +121,7 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
- returns: The updated operations.
*/
public func addUnique<V>(_ key: (String, WritableKeyPath<T, [V]?>),
objects: [V]) throws -> Self where V: Encodable, V: Hashable {
objects: [V]) -> Self where V: Encodable, V: Hashable {
var mutableOperation = self
mutableOperation.operations[key.0] = AddUnique(objects: objects)
var values = target[keyPath: key.1] ?? []
Expand Down Expand Up @@ -141,7 +151,7 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
- returns: The updated operations.
*/
public func add<V>(_ key: (String, WritableKeyPath<T, [V]>),
objects: [V]) throws -> Self where V: Encodable {
objects: [V]) -> Self where V: Encodable {
var mutableOperation = self
mutableOperation.operations[key.0] = Add(objects: objects)
var values = target[keyPath: key.1]
Expand All @@ -158,7 +168,7 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
- returns: The updated operations.
*/
public func add<V>(_ key: (String, WritableKeyPath<T, [V]?>),
objects: [V]) throws -> Self where V: Encodable {
objects: [V]) -> Self where V: Encodable {
var mutableOperation = self
mutableOperation.operations[key.0] = Add(objects: objects)
var values = target[keyPath: key.1] ?? []
Expand Down Expand Up @@ -237,7 +247,7 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
- returns: The updated operations.
*/
public func remove<V>(_ key: (String, WritableKeyPath<T, [V]>),
objects: [V]) throws -> Self where V: Encodable, V: Hashable {
objects: [V]) -> Self where V: Encodable, V: Hashable {
var mutableOperation = self
mutableOperation.operations[key.0] = Remove(objects: objects)
let values = target[keyPath: key.1]
Expand All @@ -258,7 +268,7 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
- returns: The updated operations.
*/
public func remove<V>(_ key: (String, WritableKeyPath<T, [V]?>),
objects: [V]) throws -> Self where V: Encodable, V: Hashable {
objects: [V]) -> Self where V: Encodable, V: Hashable {
var mutableOperation = self
mutableOperation.operations[key.0] = Remove(objects: objects)
let values = target[keyPath: key.1]
Expand Down Expand Up @@ -357,6 +367,11 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
let encoder = container.superEncoder(forKey: .key(key))
try value.encode(to: encoder)
}
try keysToNull.forEach { key in
let encoder = container.superEncoder(forKey: .key(key))
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
}

Expand Down
56 changes: 48 additions & 8 deletions Sources/ParseSwift/Types/QueryConstraint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation

/// Used to constrain a query.
public struct QueryConstraint: Encodable, Equatable {
public struct QueryConstraint: Encodable, Hashable {
enum Comparator: String, CodingKey, Encodable {
case lessThan = "$lt"
case lessThanOrEqualTo = "$lte"
Expand Down Expand Up @@ -48,24 +48,46 @@ public struct QueryConstraint: Encodable, Equatable {
}

var key: String
var value: Encodable
var value: Encodable?
var comparator: Comparator?
var isNull: Bool = false

public func encode(to encoder: Encoder) throws {
if let value = value as? Date {
if isNull {
var container = encoder.singleValueContainer()
try container.encodeNil()
} else if let value = value as? Date {
// Parse uses special case for date
try value.parseRepresentation.encode(to: encoder)
} else {
try value.encode(to: encoder)
try value?.encode(to: encoder)
}
}

public static func == (lhs: QueryConstraint, rhs: QueryConstraint) -> Bool {
guard lhs.key == rhs.key,
lhs.comparator == rhs.comparator else {
lhs.comparator == rhs.comparator,
lhs.isNull == rhs.isNull else {
return false
}
return lhs.value.isEqual(rhs.value)
guard let lhsValue = lhs.value,
let rhsValue = rhs.value else {
guard lhs.value == nil,
rhs.value == nil else {
return false
}
return true
}
return lhsValue.isEqual(rhsValue)
}

public func hash(into hasher: inout Hasher) {
do {
let encodedData = try ParseCoding.jsonEncoder().encode(self)
hasher.combine(encodedData)
} catch {
hasher.combine(0)
}
}
}

Expand Down Expand Up @@ -668,7 +690,25 @@ public func hasSuffix(key: String, suffix: String, modifiers: String? = nil) ->
}

/**
Add a constraint that requires a particular key exists.
Add a constraint that requires that a key is equal to **null** or **undefined**.
- parameter key: The key that the value is stored in.
- returns: The same instance of `QueryConstraint` as the receiver.
*/
public func isNull (key: String) -> QueryConstraint {
QueryConstraint(key: key, isNull: true)
}

/**
Add a constraint that requires that a key is not equal to **null** or **undefined**.
- parameter key: The key that the value is stored in.
- returns: The same instance of `QueryConstraint` as the receiver.
*/
public func notNull (key: String) -> QueryConstraint {
QueryConstraint(key: key, comparator: .notEqualTo, isNull: true)
}

/**
Add a constraint that requires a particular key to be equal to **undefined**.
- parameter key: The key that should exist.
- returns: The resulting `QueryConstraint`.
*/
Expand All @@ -677,7 +717,7 @@ public func exists(key: String) -> QueryConstraint {
}

/**
Add a constraint that requires a key not exist.
Add a constraint that requires a key to not be equal to **undefined**.
- parameter key: The key that should not exist.
- returns: The resulting `QueryConstraint`.
*/
Expand Down
4 changes: 2 additions & 2 deletions Sources/ParseSwift/Types/QueryWhere.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
import Foundation

struct QueryWhere: Encodable, Equatable {
var constraints = [String: [QueryConstraint]]()
var constraints = [String: Set<QueryConstraint>]()

mutating func add(_ constraint: QueryConstraint) {
var existing = constraints[constraint.key] ?? []
existing.append(constraint)
existing.insert(constraint)
constraints[constraint.key] = existing
}

Expand Down
Loading