Skip to content
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

Create a generic CoreDataQuery type #19394

Closed
wants to merge 1 commit into from
Closed
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
8 changes: 4 additions & 4 deletions WordPress/Classes/Models/Blog+History.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ extension Blog {
return nil
}

return try? BlogQuery()
return try? CoreDataQuery<Blog>.default()
.visible(true)
.hostname(matching: url)
.blog(in: context)
.first(in: context)
}

private static func firstBlog(in context: NSManagedObjectContext) -> Blog? {
try? BlogQuery()
try? CoreDataQuery<Blog>.default()
.visible(true)
.blog(in: context)
.first(in: context)
}

}
8 changes: 4 additions & 4 deletions WordPress/Classes/Models/Blog+Lookup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public extension Blog {
/// - Returns: The `Blog` object associated with the given `hostname`, if it exists.
@objc(lookupWithHostname:inContext:)
static func lookup(hostname: String, in context: NSManagedObjectContext) -> Blog? {
try? BlogQuery().hostname(containing: hostname).blog(in: context)
try? CoreDataQuery<Blog>.default().hostname(containing: hostname).first(in: context)
}

/// Lookup a Blog by WP.ORG Credentials
Expand All @@ -72,16 +72,16 @@ public extension Blog {

@objc(countInContext:)
static func count(in context: NSManagedObjectContext) -> Int {
BlogQuery().count(in: context)
CoreDataQuery<Blog>.default().count(in: context)
}

@objc(wpComBlogCountInContext:)
static func wpComBlogCount(in context: NSManagedObjectContext) -> Int {
BlogQuery().hostedByWPCom(true).count(in: context)
CoreDataQuery<Blog>.default().hostedByWPCom(true).count(in: context)
}

@objc(selfHostedInContext:)
static func selfHosted(in context: NSManagedObjectContext) -> [Blog] {
(try? BlogQuery().hostedByWPCom(false).blogs(in: context)) ?? []
(try? CoreDataQuery<Blog>.default().hostedByWPCom(false).result(in: context)) ?? []
}
}
12 changes: 7 additions & 5 deletions WordPress/Classes/Models/Blog+Post.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,26 @@ public extension Blog {
/// - Returns: The `AbstractPost` associated with the given post ID.
@objc(lookupPostWithID:inContext:)
func lookupPost(withID postID: NSNumber, in context: NSManagedObjectContext) -> AbstractPost? {
lookupPost(withID: postID.int64Value, in: context)
try? AbstractPost.query()
.equal(\.blog, self)
.null(\.original)
.equal(\.postID, postID)
.first(in: context)
}

/// Lookup a post in the blog.
///
/// - Parameter postID: The ID associated with the post.
/// - Returns: The `AbstractPost` associated with the given post ID.
func lookupPost(withID postID: Int, in context: NSManagedObjectContext) -> AbstractPost? {
lookupPost(withID: Int64(postID), in: context)
lookupPost(withID: postID as NSNumber, in: context)
}

/// Lookup a post in the blog.
///
/// - Parameter postID: The ID associated with the post.
/// - Returns: The `AbstractPost` associated with the given post ID.
func lookupPost(withID postID: Int64, in context: NSManagedObjectContext) -> AbstractPost? {
let request = NSFetchRequest<AbstractPost>(entityName: NSStringFromClass(AbstractPost.self))
request.predicate = NSPredicate(format: "blog = %@ AND original = NULL AND postID = %ld", self, postID)
return (try? context.fetch(request))?.first
lookupPost(withID: postID as NSNumber, in: context)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ extension BlogService {
if site.isSelfHostedWithoutJetpack, let xmlRPC = site.xmlRPC {
blog = service.findBlog(withXmlrpc: xmlRPC, andUsername: site.username)
} else {
blog = try? BlogQuery().blogID(site.siteID).username(site.username).blog(in: context)
blog = try? CoreDataQuery<Blog>.default().blogID(site.siteID).username(site.username).first(in: context)
}

return blog
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ open class NotificationSettingsService: LocalCoreDataService {
///
fileprivate func settingsFromRemote(_ remoteSettings: [RemoteNotificationSettings]) -> [NotificationSettings] {
var parsed = [NotificationSettings]()
let blogs = ((try? BlogQuery().blogs(in: managedObjectContext)) ?? []).filter { $0.dotComID != nil }
let blogs = ((try? CoreDataQuery<Blog>.default().result(in: managedObjectContext)) ?? []).filter { $0.dotComID != nil }
let blogMap = Dictionary(blogs.map { ($0.dotComID!.intValue, $0) }, uniquingKeysWith: { _, new in new })

for remoteSetting in remoteSettings {
Expand Down
4 changes: 2 additions & 2 deletions WordPress/Classes/Stores/StatsWidgetsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ private extension StatsWidgetsStore {
guard let currentData = T.read() else {
return nil
}
let updatedSiteList = (try? BlogQuery().visible(true).hostedByWPCom(true).blogs(in: blogService.managedObjectContext)) ?? []
let updatedSiteList = (try? CoreDataQuery<Blog>.default().visible(true).hostedByWPCom(true).result(in: blogService.managedObjectContext)) ?? []

let newData = updatedSiteList.reduce(into: [Int: T]()) { sitesList, site in
guard let blogID = site.dotComID else {
Expand Down Expand Up @@ -203,7 +203,7 @@ private extension StatsWidgetsStore {
}

func initializeHomeWidgetData<T: HomeWidgetData>(type: T.Type) -> [Int: T] {
let blogs = (try? BlogQuery().visible(true).hostedByWPCom(true).blogs(in: blogService.managedObjectContext)) ?? []
let blogs = (try? CoreDataQuery<Blog>.default().visible(true).hostedByWPCom(true).result(in: blogService.managedObjectContext)) ?? []
return blogs.reduce(into: [Int: T]()) { result, element in
if let blogID = element.dotComID,
let url = element.url,
Expand Down
4 changes: 2 additions & 2 deletions WordPress/Classes/Utility/AccountHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import Foundation
let context = ContextManager.sharedInstance().mainContext
let blogService = BlogService(managedObjectContext: context)

return BlogQuery().hostedByWPCom(false).count(in: context) == 0 && blogService.hasAnyJetpackBlogs() == false
return CoreDataQuery<Blog>.default().hostedByWPCom(false).count(in: context) == 0 && blogService.hasAnyJetpackBlogs() == false
}

static var hasBlogs: Bool {
Expand All @@ -40,7 +40,7 @@ import Foundation
}

static func logBlogsAndAccounts(context: NSManagedObjectContext) {
let allBlogs = (try? BlogQuery().blogs(in: context)) ?? []
let allBlogs = (try? CoreDataQuery<Blog>.default().result(in: context)) ?? []
let blogsByAccount = Dictionary(grouping: allBlogs, by: { $0.account })

let defaultAccount = try? WPAccount.lookupDefaultWordPressComAccount(in: context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ class AutomatedTransferHelper {

let service = BlogService.withMainContext()

guard let blog = try? BlogQuery().blogID(site.siteID).username(site.username).blog(in: ContextManager.sharedInstance().mainContext) else {
guard let blog = try? CoreDataQuery<Blog>.default().blogID(site.siteID).username(site.username).first(in: ContextManager.sharedInstance().mainContext) else {
DDLogInfo("[AT] Couldn't find a blog with provided JetpackSiteRef. This definitely shouldn't have happened. Bailing.")

SVProgressHUD.dismiss()
Expand Down
65 changes: 0 additions & 65 deletions WordPress/Classes/Utility/BlogQuery.swift

This file was deleted.

146 changes: 146 additions & 0 deletions WordPress/Classes/Utility/CoreDataQuery.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import Foundation

struct CoreDataQuery<Result: NSManagedObject> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Result here is confusing to me because of the Result<T, Error> type. WDYT?

private var predicates = [NSPredicate]()
private var sortDescriptors = [NSSortDescriptor]()

private var includesSubentities: Bool = true

init() {
}

func ascending(by key: String) -> Self {
var query = self
query.sortDescriptors.append(NSSortDescriptor(key: key, ascending: true))
return query
}

func descending(by key: String) -> Self {
var query = self
query.sortDescriptors.append(NSSortDescriptor(key: key, ascending: false))
return query
}

func includesSubentities(_ value: Bool) -> Self {
var query = self
query.includesSubentities = value
return query
}

func count(in context: NSManagedObjectContext) -> Int {
(try? context.count(for: buildFetchRequest())) ?? 0
Copy link
Contributor

@jkmassel jkmassel Oct 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be a bit more work, but let's not eat the error here – we should pass it up to the caller to deal with (for now we can have the same implementation at a higher level, but there's a difference between "there are no cells" and "there was an error trying to fetch the data", and the higher we can propagate that error, the more likely we can help the user understand what's going on)

}

func first(in context: NSManagedObjectContext) throws -> Result? {
let request = buildFetchRequest()
request.fetchLimit = 1
return (try context.fetch(request).first)
}

func result(in context: NSManagedObjectContext) throws -> [Result] {
try context.fetch(buildFetchRequest())
}

private func buildFetchRequest() -> NSFetchRequest<Result> {
let request = NSFetchRequest<Result>(entityName: Result.entityName())
request.includesSubentities = includesSubentities
request.sortDescriptors = sortDescriptors
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
return request
}

func and(_ predicate: NSPredicate) -> Self {
var query = self
query.predicates.append(predicate)
return query
}
}

extension CoreDataQuery {
private func compare<Value>(_ keyPath: KeyPath<Result, Value>, type: NSComparisonPredicate.Operator, _ value: Value?, options: NSComparisonPredicate.Options = []) -> Self {
and(
NSComparisonPredicate(
leftExpression: NSExpression(forKeyPath: keyPath),
rightExpression: NSExpression(forConstantValue: value),
modifier: .direct,
type: type,
options: options
)
)
}

func contains<Value>(_ keyPath: KeyPath<Result, Value>, _ value: Value) -> Self {
compare(keyPath, type: .contains, value)
}

func equal<Value>(_ keyPath: KeyPath<Result, Value>, _ value: Value) -> Self {
compare(keyPath, type: .equalTo, value)
}

func null<Value>(_ keyPath: KeyPath<Result, Value>) -> Self {
compare(keyPath, type: .equalTo, nil)
}

func notNull<Value>(_ keyPath: KeyPath<Result, Value>) -> Self {
compare(keyPath, type: .notEqualTo, nil)
}

func order<Value>(by keyPath: KeyPath<Result, Value>, ascending: Bool = true) -> Self {
let property = NSExpression(forKeyPath: keyPath).keyPath
return ascending ? self.ascending(by: property) : self.descending(by: property)
}
}

protocol CoreDataQueryable {
associatedtype CoreDataQueryResult: NSManagedObject

static func query() -> CoreDataQuery<CoreDataQueryResult>
}

extension NSManagedObject: CoreDataQueryable {}

extension CoreDataQueryable where Self: NSManagedObject {
static func query() -> CoreDataQuery<Self> {
return CoreDataQuery<Self>()
}
}

extension CoreDataQuery where Result == Blog {

static func `default`() -> CoreDataQuery<Blog> {
Blog.query().order(by: \.settings?.name).includesSubentities(false)
}

func blogID(_ id: Int) -> Self {
blogID(id as NSNumber)
}

func blogID(_ id: NSNumber) -> Self {
equal(\.blogID, id)
}

func blogID(_ id: Int64) -> Self {
blogID(id as NSNumber)
}

func username(_ username: String) -> Self {
equal(\.account?.username, username)
}

func hostname(containing hostname: String) -> Self {
contains(\.url, hostname)
}

func hostname(matching hostname: String) -> Self {
equal(\.url, hostname)
}

func visible(_ flag: Bool) -> Self {
equal(\.visible, flag)
}

func hostedByWPCom(_ flag: Bool) -> Self {
flag ? notNull(\.account) : null(\.account)
}

}
2 changes: 1 addition & 1 deletion WordPress/Classes/Utility/Editor/GutenbergRollout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ struct GutenbergRollout {
}

private func atLeastOneSiteHasAztecEnabled() -> Bool {
let allBlogs = (try? BlogQuery().blogs(in: context)) ?? []
let allBlogs = (try? CoreDataQuery<Blog>.default().result(in: context)) ?? []
return allBlogs.contains { $0.editor == .aztec }
}
}
2 changes: 1 addition & 1 deletion WordPress/Classes/Utility/Editor/GutenbergSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class GutenbergSettings {
}

private func setGutenbergEnabledForAllSites() {
let allBlogs = (try? BlogQuery().blogs(in: context)) ?? []
let allBlogs = (try? CoreDataQuery<Blog>.default().result(in: context)) ?? []
allBlogs.forEach { blog in
if blog.editor == .aztec {
setShowPhase2Dialog(true, for: blog)
Expand Down
Loading