Convert Articles, ArticlesDatabase, and SyncDatabase to Swift Packages

This commit is contained in:
Maurice Parker
2020-07-30 04:54:21 -05:00
parent e3e5d69b9b
commit fbfdbb04c7
44 changed files with 91 additions and 1928 deletions

View File

@@ -1,129 +0,0 @@
//
// Article.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/1/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public typealias ArticleSetBlock = (Set<Article>) -> Void
public struct Article: Hashable {
public let articleID: String // Unique database ID (possibly sync service ID)
public let accountID: String
public let webFeedID: String // Likely a URL, but not necessarily
public let uniqueID: String // Unique per feed (RSS guid, for example)
public let title: String?
public let contentHTML: String?
public let contentText: String?
public let url: String?
public let externalURL: String?
public let summary: String?
public let imageURL: String?
public let datePublished: Date?
public let dateModified: Date?
public let authors: Set<Author>?
public let status: ArticleStatus
public init(accountID: String, articleID: String?, webFeedID: String, uniqueID: String, title: String?, contentHTML: String?, contentText: String?, url: String?, externalURL: String?, summary: String?, imageURL: String?, datePublished: Date?, dateModified: Date?, authors: Set<Author>?, status: ArticleStatus) {
self.accountID = accountID
self.webFeedID = webFeedID
self.uniqueID = uniqueID
self.title = title
self.contentHTML = contentHTML
self.contentText = contentText
self.url = url
self.externalURL = externalURL
self.summary = summary
self.imageURL = imageURL
self.datePublished = datePublished
self.dateModified = dateModified
self.authors = authors
self.status = status
if let articleID = articleID {
self.articleID = articleID
}
else {
self.articleID = Article.calculatedArticleID(webFeedID: webFeedID, uniqueID: uniqueID)
}
}
public static func calculatedArticleID(webFeedID: String, uniqueID: String) -> String {
return databaseIDWithString("\(webFeedID) \(uniqueID)")
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(articleID)
}
// MARK: - Equatable
static public func ==(lhs: Article, rhs: Article) -> Bool {
return lhs.articleID == rhs.articleID && lhs.accountID == rhs.accountID && lhs.webFeedID == rhs.webFeedID && lhs.uniqueID == rhs.uniqueID && lhs.title == rhs.title && lhs.contentHTML == rhs.contentHTML && lhs.contentText == rhs.contentText && lhs.url == rhs.url && lhs.externalURL == rhs.externalURL && lhs.summary == rhs.summary && lhs.imageURL == rhs.imageURL && lhs.datePublished == rhs.datePublished && lhs.dateModified == rhs.dateModified && lhs.authors == rhs.authors
}
}
public extension Set where Element == Article {
func articleIDs() -> Set<String> {
return Set<String>(map { $0.articleID })
}
func unreadArticles() -> Set<Article> {
let articles = self.filter { !$0.status.read }
return Set(articles)
}
func contains(accountID: String, articleID: String) -> Bool {
return contains(where: { $0.accountID == accountID && $0.articleID == articleID})
}
}
public extension Array where Element == Article {
func articleIDs() -> [String] {
return map { $0.articleID }
}
}
public extension Article {
private static let allowedTags: Set = ["b", "bdi", "bdo", "cite", "code", "del", "dfn", "em", "i", "ins", "kbd", "mark", "q", "s", "samp", "small", "strong", "sub", "sup", "time", "u", "var"]
func sanitizedTitle(forHTML: Bool = true) -> String? {
guard let title = title else { return nil }
let scanner = Scanner(string: title)
scanner.charactersToBeSkipped = nil
var result = ""
result.reserveCapacity(title.count)
while !scanner.isAtEnd {
if let text = scanner.scanUpToString("<") {
result.append(text)
}
if let _ = scanner.scanString("<") {
// All the allowed tags currently don't allow attributes
if let tag = scanner.scanUpToString(">") {
if Self.allowedTags.contains(tag.replacingOccurrences(of: "/", with: "")) {
forHTML ? result.append("<\(tag)>") : result.append("")
} else {
forHTML ? result.append("&lt;\(tag)&gt;") : result.append("<\(tag)>")
}
let _ = scanner.scanString(">")
}
}
}
return result
}
}

View File

@@ -1,84 +0,0 @@
//
// ArticleStatus.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/1/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
// Threading rules:
// * Main-thread only
// * Except: may be created on background thread by StatusesTable.
// Which is safe, because at creation time itt not yet shared,
// and it wont be mutated ever on a background thread.
public final class ArticleStatus: Hashable {
public enum Key: String {
case read = "read"
case starred = "starred"
}
public let articleID: String
public let dateArrived: Date
public var read = false
public var starred = false
public init(articleID: String, read: Bool, starred: Bool, dateArrived: Date) {
self.articleID = articleID
self.read = read
self.starred = starred
self.dateArrived = dateArrived
}
public convenience init(articleID: String, read: Bool, dateArrived: Date) {
self.init(articleID: articleID, read: read, starred: false, dateArrived: dateArrived)
}
public func boolStatus(forKey key: ArticleStatus.Key) -> Bool {
switch key {
case .read:
return read
case .starred:
return starred
}
}
public func setBoolStatus(_ status: Bool, forKey key: ArticleStatus.Key) {
switch key {
case .read:
read = status
case .starred:
starred = status
}
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(articleID)
}
// MARK: - Equatable
public static func ==(lhs: ArticleStatus, rhs: ArticleStatus) -> Bool {
return lhs.articleID == rhs.articleID && lhs.dateArrived == rhs.dateArrived && lhs.read == rhs.read && lhs.starred == rhs.starred
}
}
public extension Set where Element == ArticleStatus {
func articleIDs() -> Set<String> {
return Set<String>(map { $0.articleID })
}
}
public extension Array where Element == ArticleStatus {
func articleIDs() -> [String] {
return map { $0.articleID }
}
}

View File

@@ -1,463 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
5102AE8724D180E50050839C /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 5102AE8624D180E50050839C /* RSCore */; };
5102AE8824D180E50050839C /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5102AE8624D180E50050839C /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
840405CA1F1A8E4300DF0296 /* DatabaseID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840405C91F1A8E4300DF0296 /* DatabaseID.swift */; };
844BEE651F0AB3C9004AB7CD /* Articles.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844BEE5B1F0AB3C8004AB7CD /* Articles.framework */; };
844BEE6A1F0AB3C9004AB7CD /* DataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE691F0AB3C9004AB7CD /* DataTests.swift */; };
844BEE7F1F0AB4CA004AB7CD /* Article.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE7E1F0AB4CA004AB7CD /* Article.swift */; };
844BEE811F0AB4D0004AB7CD /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE801F0AB4D0004AB7CD /* Author.swift */; };
844BEE851F0AB4DB004AB7CD /* ArticleStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE841F0AB4DB004AB7CD /* ArticleStatus.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
844BEE661F0AB3C9004AB7CD /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 844BEE521F0AB3C8004AB7CD /* Project object */;
proxyType = 1;
remoteGlobalIDString = 844BEE5A1F0AB3C8004AB7CD;
remoteInfo = Data;
};
844BEEA21F0AB512004AB7CD /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 844BEE9C1F0AB512004AB7CD /* RSCore.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 84CFF4F41AC3C69700CEA6C8;
remoteInfo = RSCore;
};
844BEEA41F0AB512004AB7CD /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 844BEE9C1F0AB512004AB7CD /* RSCore.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 84CFF4FF1AC3C69700CEA6C8;
remoteInfo = RSCoreTests;
};
844BEEA61F0AB512004AB7CD /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 844BEE9C1F0AB512004AB7CD /* RSCore.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 842DD7BC1E14993900E061EB;
remoteInfo = RSCoreiOS;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
5102AE8924D180E60050839C /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
5102AE8824D180E50050839C /* RSCore in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
840405C91F1A8E4300DF0296 /* DatabaseID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseID.swift; sourceTree = "<group>"; };
844BEE5B1F0AB3C8004AB7CD /* Articles.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Articles.framework; sourceTree = BUILT_PRODUCTS_DIR; };
844BEE641F0AB3C9004AB7CD /* ArticlesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ArticlesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
844BEE691F0AB3C9004AB7CD /* DataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTests.swift; sourceTree = "<group>"; };
844BEE6B1F0AB3C9004AB7CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
844BEE761F0AB444004AB7CD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
844BEE7E1F0AB4CA004AB7CD /* Article.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Article.swift; sourceTree = "<group>"; };
844BEE801F0AB4D0004AB7CD /* Author.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Author.swift; sourceTree = "<group>"; };
844BEE841F0AB4DB004AB7CD /* ArticleStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleStatus.swift; sourceTree = "<group>"; };
844BEE9C1F0AB512004AB7CD /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = "<group>"; };
848E3EB320FBCFAE0004B7ED /* RSCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
84C490F51F705D5F003131D2 /* RSWeb.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSWeb.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D511EEE020242DFB00712EC3 /* ArticlesDataTests_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ArticlesDataTests_target.xcconfig; sourceTree = "<group>"; };
D511EEE120242DFB00712EC3 /* Articles_project_debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Articles_project_debug.xcconfig; sourceTree = "<group>"; };
D511EEE220242DFB00712EC3 /* Articles_project.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Articles_project.xcconfig; sourceTree = "<group>"; };
D511EEE320242DFB00712EC3 /* Articles_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Articles_target.xcconfig; sourceTree = "<group>"; };
D511EEE420242DFB00712EC3 /* Articles_project_release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Articles_project_release.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
844BEE571F0AB3C8004AB7CD /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5102AE8724D180E50050839C /* RSCore in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
844BEE611F0AB3C9004AB7CD /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
844BEE651F0AB3C9004AB7CD /* Articles.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
844BEE511F0AB3C8004AB7CD = {
isa = PBXGroup;
children = (
844BEE7E1F0AB4CA004AB7CD /* Article.swift */,
844BEE801F0AB4D0004AB7CD /* Author.swift */,
844BEE841F0AB4DB004AB7CD /* ArticleStatus.swift */,
840405C91F1A8E4300DF0296 /* DatabaseID.swift */,
844BEE761F0AB444004AB7CD /* Info.plist */,
844BEE681F0AB3C9004AB7CD /* DataTests */,
844BEE5C1F0AB3C8004AB7CD /* Products */,
844BEEA81F0AB520004AB7CD /* Frameworks */,
D511EEE520242DFB00712EC3 /* xcconfig */,
);
sourceTree = "<group>";
usesTabs = 1;
};
844BEE5C1F0AB3C8004AB7CD /* Products */ = {
isa = PBXGroup;
children = (
844BEE5B1F0AB3C8004AB7CD /* Articles.framework */,
844BEE641F0AB3C9004AB7CD /* ArticlesTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
844BEE681F0AB3C9004AB7CD /* DataTests */ = {
isa = PBXGroup;
children = (
844BEE691F0AB3C9004AB7CD /* DataTests.swift */,
844BEE6B1F0AB3C9004AB7CD /* Info.plist */,
);
path = DataTests;
sourceTree = "<group>";
};
844BEE9D1F0AB512004AB7CD /* Products */ = {
isa = PBXGroup;
children = (
844BEEA31F0AB512004AB7CD /* RSCore.framework */,
844BEEA51F0AB512004AB7CD /* RSCoreTests.xctest */,
844BEEA71F0AB512004AB7CD /* RSCore.framework */,
);
name = Products;
sourceTree = "<group>";
};
844BEEA81F0AB520004AB7CD /* Frameworks */ = {
isa = PBXGroup;
children = (
848E3EB320FBCFAE0004B7ED /* RSCore.framework */,
84C490F51F705D5F003131D2 /* RSWeb.framework */,
844BEE9C1F0AB512004AB7CD /* RSCore.xcodeproj */,
);
name = Frameworks;
sourceTree = "<group>";
};
D511EEE520242DFB00712EC3 /* xcconfig */ = {
isa = PBXGroup;
children = (
D511EEE220242DFB00712EC3 /* Articles_project.xcconfig */,
D511EEE120242DFB00712EC3 /* Articles_project_debug.xcconfig */,
D511EEE420242DFB00712EC3 /* Articles_project_release.xcconfig */,
D511EEE320242DFB00712EC3 /* Articles_target.xcconfig */,
D511EEE020242DFB00712EC3 /* ArticlesDataTests_target.xcconfig */,
);
path = xcconfig;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
844BEE581F0AB3C8004AB7CD /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
844BEE5A1F0AB3C8004AB7CD /* Articles */ = {
isa = PBXNativeTarget;
buildConfigurationList = 844BEE6F1F0AB3C9004AB7CD /* Build configuration list for PBXNativeTarget "Articles" */;
buildPhases = (
844BEE561F0AB3C8004AB7CD /* Sources */,
844BEE571F0AB3C8004AB7CD /* Frameworks */,
844BEE581F0AB3C8004AB7CD /* Headers */,
844BEE591F0AB3C8004AB7CD /* Resources */,
51C8F34B234FB11A0048ED95 /* Run Script: Verify No Build Settings */,
5102AE8924D180E60050839C /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Articles;
packageProductDependencies = (
5102AE8624D180E50050839C /* RSCore */,
);
productName = Data;
productReference = 844BEE5B1F0AB3C8004AB7CD /* Articles.framework */;
productType = "com.apple.product-type.framework";
};
844BEE631F0AB3C9004AB7CD /* ArticlesTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 844BEE721F0AB3C9004AB7CD /* Build configuration list for PBXNativeTarget "ArticlesTests" */;
buildPhases = (
844BEE601F0AB3C9004AB7CD /* Sources */,
844BEE611F0AB3C9004AB7CD /* Frameworks */,
844BEE621F0AB3C9004AB7CD /* Resources */,
);
buildRules = (
);
dependencies = (
844BEE671F0AB3C9004AB7CD /* PBXTargetDependency */,
);
name = ArticlesTests;
productName = DataTests;
productReference = 844BEE641F0AB3C9004AB7CD /* ArticlesTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
844BEE521F0AB3C8004AB7CD /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0830;
LastUpgradeCheck = 0930;
ORGANIZATIONNAME = "Ranchero Software";
TargetAttributes = {
844BEE5A1F0AB3C8004AB7CD = {
CreatedOnToolsVersion = 8.3.2;
DevelopmentTeam = SHJK2V3AJG;
LastSwiftMigration = 0830;
ProvisioningStyle = Automatic;
};
844BEE631F0AB3C9004AB7CD = {
CreatedOnToolsVersion = 8.3.2;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 844BEE551F0AB3C8004AB7CD /* Build configuration list for PBXProject "Articles" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
);
mainGroup = 844BEE511F0AB3C8004AB7CD;
packageReferences = (
5102AE8524D180E50050839C /* XCRemoteSwiftPackageReference "RSCore" */,
);
productRefGroup = 844BEE5C1F0AB3C8004AB7CD /* Products */;
projectDirPath = "";
projectReferences = (
{
ProductGroup = 844BEE9D1F0AB512004AB7CD /* Products */;
ProjectRef = 844BEE9C1F0AB512004AB7CD /* RSCore.xcodeproj */;
},
);
projectRoot = "";
targets = (
844BEE5A1F0AB3C8004AB7CD /* Articles */,
844BEE631F0AB3C9004AB7CD /* ArticlesTests */,
);
};
/* End PBXProject section */
/* Begin PBXReferenceProxy section */
844BEEA31F0AB512004AB7CD /* RSCore.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = RSCore.framework;
remoteRef = 844BEEA21F0AB512004AB7CD /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
844BEEA51F0AB512004AB7CD /* RSCoreTests.xctest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
path = RSCoreTests.xctest;
remoteRef = 844BEEA41F0AB512004AB7CD /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
844BEEA71F0AB512004AB7CD /* RSCore.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = RSCore.framework;
remoteRef = 844BEEA61F0AB512004AB7CD /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
844BEE591F0AB3C8004AB7CD /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
844BEE621F0AB3C9004AB7CD /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
51C8F34B234FB11A0048ED95 /* Run Script: Verify No Build Settings */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Run Script: Verify No Build Settings";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [ \"$ENABLE_PREVIEWS\" = \"YES\" ]; then exit 0; fi\n\nxcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n\nif [ $? -ne 0 ]\nthen\n echo \"error: Build Setting were found in the project.pbxproj file. Most likely you didn't intend to change this file and should revert it.\"\n exit 1\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
844BEE561F0AB3C8004AB7CD /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
844BEE7F1F0AB4CA004AB7CD /* Article.swift in Sources */,
844BEE811F0AB4D0004AB7CD /* Author.swift in Sources */,
840405CA1F1A8E4300DF0296 /* DatabaseID.swift in Sources */,
844BEE851F0AB4DB004AB7CD /* ArticleStatus.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
844BEE601F0AB3C9004AB7CD /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
844BEE6A1F0AB3C9004AB7CD /* DataTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
844BEE671F0AB3C9004AB7CD /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 844BEE5A1F0AB3C8004AB7CD /* Articles */;
targetProxy = 844BEE661F0AB3C9004AB7CD /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
844BEE6D1F0AB3C9004AB7CD /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EEE120242DFB00712EC3 /* Articles_project_debug.xcconfig */;
buildSettings = {
};
name = Debug;
};
844BEE6E1F0AB3C9004AB7CD /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EEE420242DFB00712EC3 /* Articles_project_release.xcconfig */;
buildSettings = {
};
name = Release;
};
844BEE701F0AB3C9004AB7CD /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EEE320242DFB00712EC3 /* Articles_target.xcconfig */;
buildSettings = {
};
name = Debug;
};
844BEE711F0AB3C9004AB7CD /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EEE320242DFB00712EC3 /* Articles_target.xcconfig */;
buildSettings = {
};
name = Release;
};
844BEE731F0AB3C9004AB7CD /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EEE020242DFB00712EC3 /* ArticlesDataTests_target.xcconfig */;
buildSettings = {
};
name = Debug;
};
844BEE741F0AB3C9004AB7CD /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EEE020242DFB00712EC3 /* ArticlesDataTests_target.xcconfig */;
buildSettings = {
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
844BEE551F0AB3C8004AB7CD /* Build configuration list for PBXProject "Articles" */ = {
isa = XCConfigurationList;
buildConfigurations = (
844BEE6D1F0AB3C9004AB7CD /* Debug */,
844BEE6E1F0AB3C9004AB7CD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
844BEE6F1F0AB3C9004AB7CD /* Build configuration list for PBXNativeTarget "Articles" */ = {
isa = XCConfigurationList;
buildConfigurations = (
844BEE701F0AB3C9004AB7CD /* Debug */,
844BEE711F0AB3C9004AB7CD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
844BEE721F0AB3C9004AB7CD /* Build configuration list for PBXNativeTarget "ArticlesTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
844BEE731F0AB3C9004AB7CD /* Debug */,
844BEE741F0AB3C9004AB7CD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
5102AE8524D180E50050839C /* XCRemoteSwiftPackageReference "RSCore" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Ranchero-Software/RSCore.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = "1.0.0-beta1";
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
5102AE8624D180E50050839C /* RSCore */ = {
isa = XCSwiftPackageProductDependency;
package = 5102AE8524D180E50050839C /* XCRemoteSwiftPackageReference "RSCore" */;
productName = RSCore;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 844BEE521F0AB3C8004AB7CD /* Project object */;
}

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:Data.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "844BEE5A1F0AB3C8004AB7CD"
BuildableName = "Articles.framework"
BlueprintName = "Articles"
ReferencedContainer = "container:Articles.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "844BEE5A1F0AB3C8004AB7CD"
BuildableName = "Articles.framework"
BlueprintName = "Articles"
ReferencedContainer = "container:Articles.xcodeproj">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "844BEE5A1F0AB3C8004AB7CD"
BuildableName = "Articles.framework"
BlueprintName = "Articles"
ReferencedContainer = "container:Articles.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,85 +0,0 @@
//
// Author.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/1/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct Author: Codable, Hashable {
public let authorID: String // calculated
public let name: String?
public let url: String?
public let avatarURL: String?
public let emailAddress: String?
public init?(authorID: String?, name: String?, url: String?, avatarURL: String?, emailAddress: String?) {
if name == nil && url == nil && emailAddress == nil {
return nil
}
self.name = name
self.url = url
self.avatarURL = avatarURL
self.emailAddress = emailAddress
if let authorID = authorID {
self.authorID = authorID
}
else {
var s = name ?? ""
s += url ?? ""
s += avatarURL ?? ""
s += emailAddress ?? ""
self.authorID = databaseIDWithString(s)
}
}
public static func authorsWithJSON(_ jsonString: String) -> Set<Author>? {
// This is JSON stored in the database, not the JSON Feed version of an author.
guard let data = jsonString.data(using: .utf8) else {
return nil
}
let decoder = JSONDecoder()
do {
let authors = try decoder.decode([Author].self, from: data)
return Set(authors)
}
catch {
assertionFailure("JSON representation of Author array could not be decoded: \(jsonString) error: \(error)")
}
return nil
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(authorID)
}
// MARK: - Equatable
static public func ==(lhs: Author, rhs: Author) -> Bool {
// The authorID is a calculation based on all the properties,
// and so its a quick shortcut to determine equality.
return lhs.authorID == rhs.authorID
}
}
extension Set where Element == Author {
public func json() -> String? {
let encoder = JSONEncoder()
do {
let jsonData = try encoder.encode(Array(self))
return String(data: jsonData, encoding: .utf8)
}
catch {
assertionFailure("JSON representation of Author array could not be encoded: \(self) error: \(error)")
}
return nil
}
}

View File

@@ -1,32 +0,0 @@
//
// DatabaseID.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/15/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import RSCore
// MD5 works because:
// * Its fast
// * Collisions arent going to happen with feed data
private var databaseIDCache = [String: String]()
private var databaseIDCacheLock = os_unfair_lock_s()
public func databaseIDWithString(_ s: String) -> String {
os_unfair_lock_lock(&databaseIDCacheLock)
defer {
os_unfair_lock_unlock(&databaseIDCacheLock)
}
if let identifier = databaseIDCache[s] {
return identifier
}
let identifier = s.md5String
databaseIDCache[s] = identifier
return identifier
}

View File

@@ -1,350 +0,0 @@
//
// ArticlesDatabase.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/20/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import RSDatabase
import RSParser
import Articles
// This file is the entirety of the public API for ArticlesDatabase.framework.
// Everything else is implementation.
// Main thread only.
public typealias UnreadCountDictionary = [String: Int] // webFeedID: unreadCount
public typealias UnreadCountDictionaryCompletionResult = Result<UnreadCountDictionary,DatabaseError>
public typealias UnreadCountDictionaryCompletionBlock = (UnreadCountDictionaryCompletionResult) -> Void
public typealias SingleUnreadCountResult = Result<Int, DatabaseError>
public typealias SingleUnreadCountCompletionBlock = (SingleUnreadCountResult) -> Void
public struct ArticleChanges {
public let newArticles: Set<Article>?
public let updatedArticles: Set<Article>?
public let deletedArticles: Set<Article>?
public init() {
self.newArticles = Set<Article>()
self.updatedArticles = Set<Article>()
self.deletedArticles = Set<Article>()
}
public init(newArticles: Set<Article>?, updatedArticles: Set<Article>?, deletedArticles: Set<Article>?) {
self.newArticles = newArticles
self.updatedArticles = updatedArticles
self.deletedArticles = deletedArticles
}
}
public typealias UpdateArticlesResult = Result<ArticleChanges, DatabaseError>
public typealias UpdateArticlesCompletionBlock = (UpdateArticlesResult) -> Void
public typealias ArticleSetResult = Result<Set<Article>, DatabaseError>
public typealias ArticleSetResultBlock = (ArticleSetResult) -> Void
public typealias ArticleIDsResult = Result<Set<String>, DatabaseError>
public typealias ArticleIDsCompletionBlock = (ArticleIDsResult) -> Void
public typealias ArticleStatusesResult = Result<Set<ArticleStatus>, DatabaseError>
public typealias ArticleStatusesResultBlock = (ArticleStatusesResult) -> Void
public final class ArticlesDatabase {
public enum RetentionStyle {
case feedBased // Local and iCloud: article retention is defined by contents of feed
case syncSystem // Feedbin, Feedly, etc.: article retention is defined by external system
}
private let articlesTable: ArticlesTable
private let queue: DatabaseQueue
private let operationQueue = MainThreadOperationQueue()
private let retentionStyle: RetentionStyle
public init(databaseFilePath: String, accountID: String, retentionStyle: RetentionStyle) {
let queue = DatabaseQueue(databasePath: databaseFilePath)
self.queue = queue
self.articlesTable = ArticlesTable(name: DatabaseTableName.articles, accountID: accountID, queue: queue, retentionStyle: retentionStyle)
self.retentionStyle = retentionStyle
try! queue.runCreateStatements(ArticlesDatabase.tableCreationStatements)
queue.runInDatabase { databaseResult in
let database = databaseResult.database!
if !self.articlesTable.containsColumn("searchRowID", in: database) {
database.executeStatements("ALTER TABLE articles add column searchRowID INTEGER;")
}
database.executeStatements("CREATE INDEX if not EXISTS articles_searchRowID on articles(searchRowID);")
database.executeStatements("DROP TABLE if EXISTS tags;DROP INDEX if EXISTS tags_tagName_index;DROP INDEX if EXISTS articles_feedID_index;DROP INDEX if EXISTS statuses_read_index;DROP TABLE if EXISTS attachments;DROP TABLE if EXISTS attachmentsLookup;")
}
DispatchQueue.main.async {
self.articlesTable.indexUnindexedArticles()
}
}
// MARK: - Fetching Articles
public func fetchArticles(_ webFeedID: String) throws -> Set<Article> {
return try articlesTable.fetchArticles(webFeedID)
}
public func fetchArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchArticles(webFeedIDs)
}
public func fetchArticles(articleIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchArticles(articleIDs: articleIDs)
}
public func fetchUnreadArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchUnreadArticles(webFeedIDs)
}
public func fetchTodayArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate())
}
public func fetchStarredArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchStarredArticles(webFeedIDs)
}
public func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchArticlesMatching(searchString, webFeedIDs)
}
public func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchArticlesMatchingWithArticleIDs(searchString, articleIDs)
}
// MARK: - Fetching Articles Async
public func fetchArticlesAsync(_ webFeedID: String, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesAsync(webFeedID, completion)
}
public func fetchArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesAsync(webFeedIDs, completion)
}
public func fetchArticlesAsync(articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesAsync(articleIDs: articleIDs, completion)
}
public func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchUnreadArticlesAsync(webFeedIDs, completion)
}
public func fetchTodayArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesSinceAsync(webFeedIDs, todayCutoffDate(), completion)
}
public func fetchedStarredArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchStarredArticlesAsync(webFeedIDs, completion)
}
public func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesMatchingAsync(searchString, webFeedIDs, completion)
}
public func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesMatchingWithArticleIDsAsync(searchString, articleIDs, completion)
}
// MARK: - Unread Counts
/// Fetch all non-zero unread counts.
public func fetchAllUnreadCounts(_ completion: @escaping UnreadCountDictionaryCompletionBlock) {
let operation = FetchAllUnreadCountsOperation(databaseQueue: queue)
operationQueue.cancelOperations(named: operation.name!)
operation.completionBlock = { operation in
let fetchOperation = operation as! FetchAllUnreadCountsOperation
completion(fetchOperation.result)
}
operationQueue.add(operation)
}
/// Fetch unread count for a single feed.
public func fetchUnreadCount(_ webFeedID: String, _ completion: @escaping SingleUnreadCountCompletionBlock) {
let operation = FetchFeedUnreadCountOperation(webFeedID: webFeedID, databaseQueue: queue, cutoffDate: articlesTable.articleCutoffDate)
operation.completionBlock = { operation in
let fetchOperation = operation as! FetchFeedUnreadCountOperation
completion(fetchOperation.result)
}
operationQueue.add(operation)
}
/// Fetch non-zero unread counts for given webFeedIDs.
public func fetchUnreadCounts(for webFeedIDs: Set<String>, _ completion: @escaping UnreadCountDictionaryCompletionBlock) {
let operation = FetchUnreadCountsForFeedsOperation(webFeedIDs: webFeedIDs, databaseQueue: queue)
operation.completionBlock = { operation in
let fetchOperation = operation as! FetchUnreadCountsForFeedsOperation
completion(fetchOperation.result)
}
operationQueue.add(operation)
}
public func fetchUnreadCountForToday(for webFeedIDs: Set<String>, completion: @escaping SingleUnreadCountCompletionBlock) {
fetchUnreadCount(for: webFeedIDs, since: todayCutoffDate(), completion: completion)
}
public func fetchUnreadCount(for webFeedIDs: Set<String>, since: Date, completion: @escaping SingleUnreadCountCompletionBlock) {
articlesTable.fetchUnreadCount(webFeedIDs, since, completion)
}
public func fetchStarredAndUnreadCount(for webFeedIDs: Set<String>, completion: @escaping SingleUnreadCountCompletionBlock) {
articlesTable.fetchStarredAndUnreadCount(webFeedIDs, completion)
}
// MARK: - Saving, Updating, and Deleting Articles
/// Update articles and save new ones  for feed-based systems (local and iCloud).
public func update(with parsedItems: Set<ParsedItem>, webFeedID: String, deleteOlder: Bool, completion: @escaping UpdateArticlesCompletionBlock) {
precondition(retentionStyle == .feedBased)
articlesTable.update(parsedItems, webFeedID, deleteOlder, completion)
}
/// Update articles and save new ones for sync systems (Feedbin, Feedly, etc.).
public func update(webFeedIDsAndItems: [String: Set<ParsedItem>], defaultRead: Bool, completion: @escaping UpdateArticlesCompletionBlock) {
precondition(retentionStyle == .syncSystem)
articlesTable.update(webFeedIDsAndItems, defaultRead, completion)
}
/// Delete articles
public func delete(articleIDs: Set<String>, completion: DatabaseCompletionBlock?) {
articlesTable.delete(articleIDs: articleIDs, completion: completion)
}
// MARK: - Status
/// Fetch the articleIDs of unread articles in feeds specified by webFeedIDs.
public func fetchUnreadArticleIDsAsync(webFeedIDs: Set<String>, completion: @escaping ArticleIDsCompletionBlock) {
articlesTable.fetchUnreadArticleIDsAsync(webFeedIDs, completion)
}
/// Fetch the articleIDs of starred articles in feeds specified by webFeedIDs.
public func fetchStarredArticleIDsAsync(webFeedIDs: Set<String>, completion: @escaping ArticleIDsCompletionBlock) {
articlesTable.fetchStarredArticleIDsAsync(webFeedIDs, completion)
}
/// Fetch articleIDs for articles that we should have, but dont. These articles are either (starred) or (newer than the article cutoff date).
public func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) {
articlesTable.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion)
}
public func mark(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) throws -> Set<ArticleStatus>? {
return try articlesTable.mark(articles, statusKey, flag)
}
public func markAndFetchNew(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping ArticleIDsCompletionBlock) {
articlesTable.markAndFetchNew(articleIDs, statusKey, flag, completion)
}
/// Create statuses for specified articleIDs. For existing statuses, dont do anything.
/// For newly-created statuses, mark them as read and not-starred.
public func createStatusesIfNeeded(articleIDs: Set<String>, completion: @escaping DatabaseCompletionBlock) {
articlesTable.createStatusesIfNeeded(articleIDs, completion)
}
#if os(iOS)
// MARK: - Suspend and Resume (for iOS)
/// Cancel current operations and close the database.
public func cancelAndSuspend() {
cancelOperations()
suspend()
}
/// Close the database and stop running database calls.
/// Any pending calls will complete first.
public func suspend() {
operationQueue.suspend()
queue.suspend()
}
/// Open the database and allow for running database calls again.
public func resume() {
queue.resume()
operationQueue.resume()
}
#endif
// MARK: - Caches
/// Call to free up some memory. Should be done when the app is backgrounded, for instance.
/// This does not empty *all* caches  just the ones that are empty-able.
public func emptyCaches() {
articlesTable.emptyCaches()
}
// MARK: - Cleanup
/// Calls the various clean-up functions. To be used only at startup.
///
/// This prevents the database from growing forever. If we didnt do this:
/// 1) The database would grow to an inordinate size, and
/// 2) the app would become very slow.
public func cleanupDatabaseAtStartup(subscribedToWebFeedIDs: Set<String>) {
if retentionStyle == .syncSystem {
articlesTable.deleteOldArticles()
}
articlesTable.deleteArticlesNotInSubscribedToFeedIDs(subscribedToWebFeedIDs)
articlesTable.deleteOldStatuses()
}
/// Do database cleanups made necessary by the retention policy change in April 2020.
///
/// The retention policy for feed-based systems changed in April 2020:
/// we keep articles only for as long as theyre in the feed.
/// This change could result in a bunch of older articles suddenly
/// appearing as unread articles.
///
/// These are articles that were in the database,
/// but werent appearing in the UI because they were beyond the 90-day window.
/// (The previous retention policy used a 90-day window.)
///
/// This function marks everything as read thats beyond that 90-day window.
/// Its intended to be called only once on an account.
public func performApril2020RetentionPolicyChange() {
precondition(retentionStyle == .feedBased)
articlesTable.markOlderStatusesAsRead()
}
}
// MARK: - Private
private extension ArticlesDatabase {
static let tableCreationStatements = """
CREATE TABLE if not EXISTS articles (articleID TEXT NOT NULL PRIMARY KEY, feedID TEXT NOT NULL, uniqueID TEXT NOT NULL, title TEXT, contentHTML TEXT, contentText TEXT, url TEXT, externalURL TEXT, summary TEXT, imageURL TEXT, bannerImageURL TEXT, datePublished DATE, dateModified DATE, searchRowID INTEGER);
CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0);
CREATE TABLE if not EXISTS authors (authorID TEXT NOT NULL PRIMARY KEY, name TEXT, url TEXT, avatarURL TEXT, emailAddress TEXT);
CREATE TABLE if not EXISTS authorsLookup (authorID TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(authorID, articleID));
CREATE INDEX if not EXISTS articles_feedID_datePublished_articleID on articles (feedID, datePublished, articleID);
CREATE INDEX if not EXISTS statuses_starred_index on statuses (starred);
CREATE VIRTUAL TABLE if not EXISTS search using fts4(title, body);
CREATE TRIGGER if not EXISTS articles_after_delete_trigger_delete_search_text after delete on articles begin delete from search where rowid = OLD.searchRowID; end;
"""
func todayCutoffDate() -> Date {
// 24 hours previous. This is used by the Today smart feed, which should not actually empty out at midnight.
return Date(timeIntervalSinceNow: -(60 * 60 * 24)) // This does not need to be more precise.
}
// MARK: - Operations
func cancelOperations() {
operationQueue.cancelAllOperations()
}
}

View File

@@ -1,717 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
5102AE8C24D181080050839C /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 5102AE8B24D181080050839C /* RSCore */; };
5102AE8D24D181080050839C /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5102AE8B24D181080050839C /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
51B0DF1724D24EC4000AD99E /* RSDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 51B0DF1624D24EC4000AD99E /* RSDatabase */; };
51B0DF1824D24EC4000AD99E /* RSDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51B0DF1624D24EC4000AD99E /* RSDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
51B0DF2924D2C837000AD99E /* RSParser in Frameworks */ = {isa = PBXBuildFile; productRef = 51B0DF2824D2C837000AD99E /* RSParser */; };
51B0DF2A24D2C837000AD99E /* RSParser in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51B0DF2824D2C837000AD99E /* RSParser */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
84116B8923E01E86000B2E98 /* FetchFeedUnreadCountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84116B8823E01E86000B2E98 /* FetchFeedUnreadCountOperation.swift */; };
841D4D742106B59F00DD04E6 /* Articles.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841D4D732106B59F00DD04E6 /* Articles.framework */; };
84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */; };
84288A021F6A3D8000395871 /* RelatedObjectsMap+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */; };
843577221F749C6200F460AE /* ArticleChangesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843577211F749C6200F460AE /* ArticleChangesTests.swift */; };
843702C31F70D15D00B18807 /* ParsedArticle+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843702C21F70D15D00B18807 /* ParsedArticle+Database.swift */; };
843CB9961F34174100EE6581 /* Author+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F20F901F1810DD00D8E682 /* Author+Database.swift */; };
844BEE411F0AB3AB004AB7CD /* ArticlesDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844BEE371F0AB3AA004AB7CD /* ArticlesDatabase.framework */; };
844BEE461F0AB3AB004AB7CD /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */; };
845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580661F0AEBCD003CCFA1 /* Constants.swift */; };
845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580751F0AF670003CCFA1 /* Article+Database.swift */; };
8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */; };
84611DCC23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84611DCB23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift */; };
8477ACBC2221E76F00DF7F37 /* SearchTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBB2221E76F00DF7F37 /* SearchTable.swift */; };
84C242C923DEB45C00C50516 /* FetchAllUnreadCountsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C242C823DEB45C00C50516 /* FetchAllUnreadCountsOperation.swift */; };
84E156EA1F0AB80500F8CC05 /* ArticlesDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156E91F0AB80500F8CC05 /* ArticlesDatabase.swift */; };
84E156EC1F0AB80E00F8CC05 /* ArticlesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */; };
84E156EE1F0AB81400F8CC05 /* StatusesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156ED1F0AB81400F8CC05 /* StatusesTable.swift */; };
84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F20F8E1F180D8700D8E682 /* AuthorsTable.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
844BEE421F0AB3AB004AB7CD /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 844BEE2E1F0AB3AA004AB7CD /* Project object */;
proxyType = 1;
remoteGlobalIDString = 844BEE361F0AB3AA004AB7CD;
remoteInfo = Database;
};
846146231F0ABC7400870CB3 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 84FF5F841EFA285800C15A01;
remoteInfo = RSParser;
};
846146251F0ABC7400870CB3 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 84FF5F8D1EFA285800C15A01;
remoteInfo = RSParserTests;
};
84BB4B971F119C4900858766 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 84BB4B8F1F119C4900858766 /* RSCore.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 84CFF4F41AC3C69700CEA6C8;
remoteInfo = RSCore;
};
84BB4B991F119C4900858766 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 84BB4B8F1F119C4900858766 /* RSCore.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 84CFF4FF1AC3C69700CEA6C8;
remoteInfo = RSCoreTests;
};
84BB4B9B1F119C4900858766 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 84BB4B8F1F119C4900858766 /* RSCore.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 842DD7BC1E14993900E061EB;
remoteInfo = RSCoreiOS;
};
84E156F71F0AB83600F8CC05 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 84E156F11F0AB83600F8CC05 /* Data.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 844BEE5B1F0AB3C8004AB7CD;
remoteInfo = Data;
};
84E156F91F0AB83600F8CC05 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 84E156F11F0AB83600F8CC05 /* Data.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 844BEE641F0AB3C9004AB7CD;
remoteInfo = DataTests;
};
84E157061F0AB89B00F8CC05 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 84E157001F0AB89B00F8CC05 /* RSDatabase.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 84F22C551B52E0D9000060CE;
remoteInfo = RSDatabase;
};
84E157081F0AB89B00F8CC05 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 84E157001F0AB89B00F8CC05 /* RSDatabase.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 84F22C5F1B52E0D9000060CE;
remoteInfo = RSDatabaseTests;
};
84E1570A1F0AB89B00F8CC05 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 84E157001F0AB89B00F8CC05 /* RSDatabase.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 8400ABF71E0CFBD800AA7C57;
remoteInfo = RSDatabaseiOS;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
5102AE8E24D181090050839C /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
51B0DF2A24D2C837000AD99E /* RSParser in Embed Frameworks */,
51B0DF1824D24EC4000AD99E /* RSDatabase in Embed Frameworks */,
5102AE8D24D181080050839C /* RSCore in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
51C451FE2264CF2100C03939 /* RSParser.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSParser.framework; sourceTree = BUILT_PRODUCTS_DIR; };
84116B8823E01E86000B2E98 /* FetchFeedUnreadCountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchFeedUnreadCountOperation.swift; sourceTree = "<group>"; };
841D4D732106B59F00DD04E6 /* Articles.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Articles.framework; sourceTree = BUILT_PRODUCTS_DIR; };
842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseObject+Database.swift"; sourceTree = "<group>"; };
84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelatedObjectsMap+Database.swift"; sourceTree = "<group>"; };
843577211F749C6200F460AE /* ArticleChangesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleChangesTests.swift; sourceTree = "<group>"; };
843702C21F70D15D00B18807 /* ParsedArticle+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "ParsedArticle+Database.swift"; path = "Extensions/ParsedArticle+Database.swift"; sourceTree = "<group>"; };
844BEE371F0AB3AA004AB7CD /* ArticlesDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ArticlesDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; };
844BEE401F0AB3AB004AB7CD /* ArticlesDatabaseTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ArticlesDatabaseTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseTests.swift; sourceTree = "<group>"; };
844BEE471F0AB3AB004AB7CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
845580661F0AEBCD003CCFA1 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
845580751F0AF670003CCFA1 /* Article+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Article+Database.swift"; path = "Extensions/Article+Database.swift"; sourceTree = "<group>"; };
845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "ArticleStatus+Database.swift"; path = "Extensions/ArticleStatus+Database.swift"; sourceTree = "<group>"; };
84611DCB23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchUnreadCountsForFeedsOperation.swift; sourceTree = "<group>"; };
8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = "<group>"; };
8477ACBB2221E76F00DF7F37 /* SearchTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTable.swift; sourceTree = "<group>"; };
848E3EB820FBCFD20004B7ED /* RSCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
848E3EBA20FBCFD80004B7ED /* RSParser.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSParser.framework; sourceTree = BUILT_PRODUCTS_DIR; };
848E3EBC20FBCFDE0004B7ED /* RSDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; };
84BB4B8F1F119C4900858766 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = "<group>"; };
84C242C823DEB45C00C50516 /* FetchAllUnreadCountsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAllUnreadCountsOperation.swift; sourceTree = "<group>"; };
84E156E81F0AB75600F8CC05 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
84E156E91F0AB80500F8CC05 /* ArticlesDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticlesDatabase.swift; sourceTree = "<group>"; };
84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticlesTable.swift; sourceTree = "<group>"; };
84E156ED1F0AB81400F8CC05 /* StatusesTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusesTable.swift; sourceTree = "<group>"; };
84E156F11F0AB83600F8CC05 /* Data.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Data.xcodeproj; path = ../Data/Data.xcodeproj; sourceTree = "<group>"; };
84E157001F0AB89B00F8CC05 /* RSDatabase.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSDatabase.xcodeproj; path = ../RSDatabase/RSDatabase.xcodeproj; sourceTree = "<group>"; };
84F20F8E1F180D8700D8E682 /* AuthorsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorsTable.swift; sourceTree = "<group>"; };
84F20F901F1810DD00D8E682 /* Author+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Author+Database.swift"; path = "Extensions/Author+Database.swift"; sourceTree = "<group>"; };
D511EEE720242E0800712EC3 /* ArticlesDatabaseTests_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ArticlesDatabaseTests_target.xcconfig; sourceTree = "<group>"; };
D511EEE820242E0800712EC3 /* ArticlesDatabase_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ArticlesDatabase_target.xcconfig; sourceTree = "<group>"; };
D511EEE920242E0800712EC3 /* ArticlesDatabase_project.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ArticlesDatabase_project.xcconfig; sourceTree = "<group>"; };
D511EEEA20242E0800712EC3 /* ArticlesDatabase_project_debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ArticlesDatabase_project_debug.xcconfig; sourceTree = "<group>"; };
D511EEEB20242E0800712EC3 /* ArticlesDatabase_project_release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ArticlesDatabase_project_release.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
844BEE331F0AB3AA004AB7CD /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
51B0DF2924D2C837000AD99E /* RSParser in Frameworks */,
51B0DF1724D24EC4000AD99E /* RSDatabase in Frameworks */,
5102AE8C24D181080050839C /* RSCore in Frameworks */,
841D4D742106B59F00DD04E6 /* Articles.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
844BEE3D1F0AB3AB004AB7CD /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
844BEE411F0AB3AB004AB7CD /* ArticlesDatabase.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
844BEE2D1F0AB3AA004AB7CD = {
isa = PBXGroup;
children = (
84E156E91F0AB80500F8CC05 /* ArticlesDatabase.swift */,
845580661F0AEBCD003CCFA1 /* Constants.swift */,
84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */,
8477ACBB2221E76F00DF7F37 /* SearchTable.swift */,
84E156ED1F0AB81400F8CC05 /* StatusesTable.swift */,
84F20F8E1F180D8700D8E682 /* AuthorsTable.swift */,
84C242C723DEB42700C50516 /* Operations */,
8461462A1F0AC44100870CB3 /* Extensions */,
84E156E81F0AB75600F8CC05 /* Info.plist */,
844BEE441F0AB3AB004AB7CD /* DatabaseTests */,
844BEE381F0AB3AA004AB7CD /* Products */,
84E156FB1F0AB83A00F8CC05 /* Frameworks */,
D511EEE620242E0800712EC3 /* xcconfig */,
);
sourceTree = "<group>";
usesTabs = 1;
};
844BEE381F0AB3AA004AB7CD /* Products */ = {
isa = PBXGroup;
children = (
844BEE371F0AB3AA004AB7CD /* ArticlesDatabase.framework */,
844BEE401F0AB3AB004AB7CD /* ArticlesDatabaseTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
844BEE441F0AB3AB004AB7CD /* DatabaseTests */ = {
isa = PBXGroup;
children = (
844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */,
843577211F749C6200F460AE /* ArticleChangesTests.swift */,
844BEE471F0AB3AB004AB7CD /* Info.plist */,
);
path = DatabaseTests;
sourceTree = "<group>";
};
8461461F1F0ABC7300870CB3 /* Products */ = {
isa = PBXGroup;
children = (
846146241F0ABC7400870CB3 /* RSParser.framework */,
846146261F0ABC7400870CB3 /* RSParserTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
8461462A1F0AC44100870CB3 /* Extensions */ = {
isa = PBXGroup;
children = (
845580751F0AF670003CCFA1 /* Article+Database.swift */,
843702C21F70D15D00B18807 /* ParsedArticle+Database.swift */,
845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */,
84F20F901F1810DD00D8E682 /* Author+Database.swift */,
842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */,
84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */,
);
name = Extensions;
sourceTree = "<group>";
};
84BB4B901F119C4900858766 /* Products */ = {
isa = PBXGroup;
children = (
84BB4B981F119C4900858766 /* RSCore.framework */,
84BB4B9A1F119C4900858766 /* RSCoreTests.xctest */,
84BB4B9C1F119C4900858766 /* RSCore.framework */,
);
name = Products;
sourceTree = "<group>";
};
84C242C723DEB42700C50516 /* Operations */ = {
isa = PBXGroup;
children = (
84116B8823E01E86000B2E98 /* FetchFeedUnreadCountOperation.swift */,
84611DCB23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift */,
84C242C823DEB45C00C50516 /* FetchAllUnreadCountsOperation.swift */,
);
path = Operations;
sourceTree = "<group>";
};
84E156F21F0AB83600F8CC05 /* Products */ = {
isa = PBXGroup;
children = (
84E156F81F0AB83600F8CC05 /* Data.framework */,
84E156FA1F0AB83600F8CC05 /* DataTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
84E156FB1F0AB83A00F8CC05 /* Frameworks */ = {
isa = PBXGroup;
children = (
51C451FE2264CF2100C03939 /* RSParser.framework */,
841D4D732106B59F00DD04E6 /* Articles.framework */,
848E3EBC20FBCFDE0004B7ED /* RSDatabase.framework */,
848E3EBA20FBCFD80004B7ED /* RSParser.framework */,
848E3EB820FBCFD20004B7ED /* RSCore.framework */,
84E156F11F0AB83600F8CC05 /* Data.xcodeproj */,
84E157001F0AB89B00F8CC05 /* RSDatabase.xcodeproj */,
84BB4B8F1F119C4900858766 /* RSCore.xcodeproj */,
8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */,
);
name = Frameworks;
sourceTree = "<group>";
};
84E157011F0AB89B00F8CC05 /* Products */ = {
isa = PBXGroup;
children = (
84E157071F0AB89B00F8CC05 /* RSDatabase.framework */,
84E157091F0AB89B00F8CC05 /* RSDatabaseTests.xctest */,
84E1570B1F0AB89B00F8CC05 /* RSDatabase.framework */,
);
name = Products;
sourceTree = "<group>";
};
D511EEE620242E0800712EC3 /* xcconfig */ = {
isa = PBXGroup;
children = (
D511EEE720242E0800712EC3 /* ArticlesDatabaseTests_target.xcconfig */,
D511EEE820242E0800712EC3 /* ArticlesDatabase_target.xcconfig */,
D511EEE920242E0800712EC3 /* ArticlesDatabase_project.xcconfig */,
D511EEEA20242E0800712EC3 /* ArticlesDatabase_project_debug.xcconfig */,
D511EEEB20242E0800712EC3 /* ArticlesDatabase_project_release.xcconfig */,
);
path = xcconfig;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
844BEE341F0AB3AA004AB7CD /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
844BEE361F0AB3AA004AB7CD /* ArticlesDatabase */ = {
isa = PBXNativeTarget;
buildConfigurationList = 844BEE4B1F0AB3AB004AB7CD /* Build configuration list for PBXNativeTarget "ArticlesDatabase" */;
buildPhases = (
844BEE321F0AB3AA004AB7CD /* Sources */,
844BEE331F0AB3AA004AB7CD /* Frameworks */,
844BEE341F0AB3AA004AB7CD /* Headers */,
844BEE351F0AB3AA004AB7CD /* Resources */,
51C8F34A234FB0F50048ED95 /* Run Script: Verify No Build Settings */,
5102AE8E24D181090050839C /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = ArticlesDatabase;
packageProductDependencies = (
5102AE8B24D181080050839C /* RSCore */,
51B0DF1624D24EC4000AD99E /* RSDatabase */,
51B0DF2824D2C837000AD99E /* RSParser */,
);
productName = Database;
productReference = 844BEE371F0AB3AA004AB7CD /* ArticlesDatabase.framework */;
productType = "com.apple.product-type.framework";
};
844BEE3F1F0AB3AB004AB7CD /* ArticlesDatabaseTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 844BEE4E1F0AB3AB004AB7CD /* Build configuration list for PBXNativeTarget "ArticlesDatabaseTests" */;
buildPhases = (
844BEE3C1F0AB3AB004AB7CD /* Sources */,
844BEE3D1F0AB3AB004AB7CD /* Frameworks */,
844BEE3E1F0AB3AB004AB7CD /* Resources */,
);
buildRules = (
);
dependencies = (
844BEE431F0AB3AB004AB7CD /* PBXTargetDependency */,
);
name = ArticlesDatabaseTests;
productName = DatabaseTests;
productReference = 844BEE401F0AB3AB004AB7CD /* ArticlesDatabaseTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
844BEE2E1F0AB3AA004AB7CD /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0830;
LastUpgradeCheck = 0930;
ORGANIZATIONNAME = "Ranchero Software";
TargetAttributes = {
844BEE361F0AB3AA004AB7CD = {
CreatedOnToolsVersion = 8.3.2;
DevelopmentTeam = SHJK2V3AJG;
LastSwiftMigration = 0830;
ProvisioningStyle = Automatic;
};
844BEE3F1F0AB3AB004AB7CD = {
CreatedOnToolsVersion = 8.3.2;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 844BEE311F0AB3AA004AB7CD /* Build configuration list for PBXProject "ArticlesDatabase" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
);
mainGroup = 844BEE2D1F0AB3AA004AB7CD;
packageReferences = (
5102AE8A24D181080050839C /* XCRemoteSwiftPackageReference "RSCore" */,
51B0DF1524D24EC4000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */,
51B0DF2724D2C837000AD99E /* XCRemoteSwiftPackageReference "RSParser" */,
);
productRefGroup = 844BEE381F0AB3AA004AB7CD /* Products */;
projectDirPath = "";
projectReferences = (
{
ProductGroup = 84E156F21F0AB83600F8CC05 /* Products */;
ProjectRef = 84E156F11F0AB83600F8CC05 /* Data.xcodeproj */;
},
{
ProductGroup = 84BB4B901F119C4900858766 /* Products */;
ProjectRef = 84BB4B8F1F119C4900858766 /* RSCore.xcodeproj */;
},
{
ProductGroup = 84E157011F0AB89B00F8CC05 /* Products */;
ProjectRef = 84E157001F0AB89B00F8CC05 /* RSDatabase.xcodeproj */;
},
{
ProductGroup = 8461461F1F0ABC7300870CB3 /* Products */;
ProjectRef = 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */;
},
);
projectRoot = "";
targets = (
844BEE361F0AB3AA004AB7CD /* ArticlesDatabase */,
844BEE3F1F0AB3AB004AB7CD /* ArticlesDatabaseTests */,
);
};
/* End PBXProject section */
/* Begin PBXReferenceProxy section */
846146241F0ABC7400870CB3 /* RSParser.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = RSParser.framework;
remoteRef = 846146231F0ABC7400870CB3 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
846146261F0ABC7400870CB3 /* RSParserTests.xctest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
path = RSParserTests.xctest;
remoteRef = 846146251F0ABC7400870CB3 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
84BB4B981F119C4900858766 /* RSCore.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = RSCore.framework;
remoteRef = 84BB4B971F119C4900858766 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
84BB4B9A1F119C4900858766 /* RSCoreTests.xctest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
path = RSCoreTests.xctest;
remoteRef = 84BB4B991F119C4900858766 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
84BB4B9C1F119C4900858766 /* RSCore.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = RSCore.framework;
remoteRef = 84BB4B9B1F119C4900858766 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
84E156F81F0AB83600F8CC05 /* Data.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = Data.framework;
remoteRef = 84E156F71F0AB83600F8CC05 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
84E156FA1F0AB83600F8CC05 /* DataTests.xctest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
path = DataTests.xctest;
remoteRef = 84E156F91F0AB83600F8CC05 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
84E157071F0AB89B00F8CC05 /* RSDatabase.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = RSDatabase.framework;
remoteRef = 84E157061F0AB89B00F8CC05 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
84E157091F0AB89B00F8CC05 /* RSDatabaseTests.xctest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
path = RSDatabaseTests.xctest;
remoteRef = 84E157081F0AB89B00F8CC05 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
84E1570B1F0AB89B00F8CC05 /* RSDatabase.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = RSDatabase.framework;
remoteRef = 84E1570A1F0AB89B00F8CC05 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
844BEE351F0AB3AA004AB7CD /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
844BEE3E1F0AB3AB004AB7CD /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
51C8F34A234FB0F50048ED95 /* Run Script: Verify No Build Settings */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Run Script: Verify No Build Settings";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [ \"$ENABLE_PREVIEWS\" = \"YES\" ]; then exit 0; fi\n\nxcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n\nif [ $? -ne 0 ]\nthen\n echo \"error: Build Setting were found in the project.pbxproj file. Most likely you didn't intend to change this file and should revert it.\"\n exit 1\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
844BEE321F0AB3AA004AB7CD /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */,
843CB9961F34174100EE6581 /* Author+Database.swift in Sources */,
845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */,
84116B8923E01E86000B2E98 /* FetchFeedUnreadCountOperation.swift in Sources */,
8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */,
84288A021F6A3D8000395871 /* RelatedObjectsMap+Database.swift in Sources */,
84C242C923DEB45C00C50516 /* FetchAllUnreadCountsOperation.swift in Sources */,
84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */,
84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */,
8477ACBC2221E76F00DF7F37 /* SearchTable.swift in Sources */,
84611DCC23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift in Sources */,
843702C31F70D15D00B18807 /* ParsedArticle+Database.swift in Sources */,
84E156EC1F0AB80E00F8CC05 /* ArticlesTable.swift in Sources */,
84E156EE1F0AB81400F8CC05 /* StatusesTable.swift in Sources */,
84E156EA1F0AB80500F8CC05 /* ArticlesDatabase.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
844BEE3C1F0AB3AB004AB7CD /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
843577221F749C6200F460AE /* ArticleChangesTests.swift in Sources */,
844BEE461F0AB3AB004AB7CD /* DatabaseTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
844BEE431F0AB3AB004AB7CD /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 844BEE361F0AB3AA004AB7CD /* ArticlesDatabase */;
targetProxy = 844BEE421F0AB3AB004AB7CD /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
844BEE491F0AB3AB004AB7CD /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EEEA20242E0800712EC3 /* ArticlesDatabase_project_debug.xcconfig */;
buildSettings = {
};
name = Debug;
};
844BEE4A1F0AB3AB004AB7CD /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EEEB20242E0800712EC3 /* ArticlesDatabase_project_release.xcconfig */;
buildSettings = {
};
name = Release;
};
844BEE4C1F0AB3AB004AB7CD /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EEE820242E0800712EC3 /* ArticlesDatabase_target.xcconfig */;
buildSettings = {
};
name = Debug;
};
844BEE4D1F0AB3AB004AB7CD /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EEE820242E0800712EC3 /* ArticlesDatabase_target.xcconfig */;
buildSettings = {
};
name = Release;
};
844BEE4F1F0AB3AB004AB7CD /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EEE720242E0800712EC3 /* ArticlesDatabaseTests_target.xcconfig */;
buildSettings = {
};
name = Debug;
};
844BEE501F0AB3AB004AB7CD /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D511EEE720242E0800712EC3 /* ArticlesDatabaseTests_target.xcconfig */;
buildSettings = {
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
844BEE311F0AB3AA004AB7CD /* Build configuration list for PBXProject "ArticlesDatabase" */ = {
isa = XCConfigurationList;
buildConfigurations = (
844BEE491F0AB3AB004AB7CD /* Debug */,
844BEE4A1F0AB3AB004AB7CD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
844BEE4B1F0AB3AB004AB7CD /* Build configuration list for PBXNativeTarget "ArticlesDatabase" */ = {
isa = XCConfigurationList;
buildConfigurations = (
844BEE4C1F0AB3AB004AB7CD /* Debug */,
844BEE4D1F0AB3AB004AB7CD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
844BEE4E1F0AB3AB004AB7CD /* Build configuration list for PBXNativeTarget "ArticlesDatabaseTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
844BEE4F1F0AB3AB004AB7CD /* Debug */,
844BEE501F0AB3AB004AB7CD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
5102AE8A24D181080050839C /* XCRemoteSwiftPackageReference "RSCore" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Ranchero-Software/RSCore.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = "1.0.0-beta1";
};
};
51B0DF1524D24EC4000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Ranchero-Software/RSDatabase.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = "1.0.0-beta1";
};
};
51B0DF2724D2C837000AD99E /* XCRemoteSwiftPackageReference "RSParser" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Ranchero-Software/RSParser.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = "2.0.0-beta1";
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
5102AE8B24D181080050839C /* RSCore */ = {
isa = XCSwiftPackageProductDependency;
package = 5102AE8A24D181080050839C /* XCRemoteSwiftPackageReference "RSCore" */;
productName = RSCore;
};
51B0DF1624D24EC4000AD99E /* RSDatabase */ = {
isa = XCSwiftPackageProductDependency;
package = 51B0DF1524D24EC4000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */;
productName = RSDatabase;
};
51B0DF2824D2C837000AD99E /* RSParser */ = {
isa = XCSwiftPackageProductDependency;
package = 51B0DF2724D2C837000AD99E /* XCRemoteSwiftPackageReference "RSParser" */;
productName = RSParser;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 844BEE2E1F0AB3AA004AB7CD /* Project object */;
}

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:Database.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1120"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "844BEE361F0AB3AA004AB7CD"
BuildableName = "ArticlesDatabase.framework"
BlueprintName = "ArticlesDatabase"
ReferencedContainer = "container:ArticlesDatabase.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "844BEE361F0AB3AA004AB7CD"
BuildableName = "ArticlesDatabase.framework"
BlueprintName = "ArticlesDatabase"
ReferencedContainer = "container:ArticlesDatabase.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +0,0 @@
//
// AuthorsTable.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/13/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import RSDatabase
import RSDatabaseObjC
import Articles
// article->authors is a many-to-many relationship.
// Theres a lookup table relating authorID and articleID.
//
// CREATE TABLE if not EXISTS authors (authorID TEXT NOT NULL PRIMARY KEY, name TEXT, url TEXT, avatarURL TEXT, emailAddress TEXT);
// CREATE TABLE if not EXISTS authorLookup (authorID TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(authorID, articleID));
final class AuthorsTable: DatabaseRelatedObjectsTable {
let name: String
let databaseIDKey = DatabaseKey.authorID
var cache = DatabaseObjectCache()
init(name: String) {
self.name = name
}
// MARK: - DatabaseRelatedObjectsTable
func objectWithRow(_ row: FMResultSet) -> DatabaseObject? {
if let author = Author(row: row) {
return author as DatabaseObject
}
return nil
}
}

View File

@@ -1,64 +0,0 @@
//
// Keys.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/3/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
// MARK: - Database structure
struct DatabaseTableName {
static let articles = "articles"
static let authors = "authors"
static let authorsLookup = "authorsLookup"
static let statuses = "statuses"
}
struct DatabaseKey {
// Shared
static let articleID = "articleID"
static let url = "url"
static let title = "title"
// Article
static let feedID = "feedID"
static let uniqueID = "uniqueID"
static let contentHTML = "contentHTML"
static let contentText = "contentText"
static let externalURL = "externalURL"
static let summary = "summary"
static let imageURL = "imageURL"
static let bannerImageURL = "bannerImageURL"
static let datePublished = "datePublished"
static let dateModified = "dateModified"
static let authors = "authors"
static let searchRowID = "searchRowID"
// ArticleStatus
static let read = "read"
static let starred = "starred"
static let dateArrived = "dateArrived"
// Tag
static let tagName = "tagName"
// Author
static let authorID = "authorID"
static let name = "name"
static let avatarURL = "avatarURL"
static let emailAddress = "emailAddress"
// Search
static let body = "body"
static let rowID = "rowid"
}
struct RelationshipName {
static let authors = "authors"
}

View File

@@ -1,19 +0,0 @@
//
// DatabaseObject+Database.swift
// NetNewsWire
//
// Created by Brent Simmons on 9/13/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import RSDatabase
import Articles
extension Array where Element == DatabaseObject {
func asAuthors() -> Set<Author>? {
let authors = Set(self.map { $0 as! Author })
return authors.isEmpty ? nil : authors
}
}

View File

@@ -1,220 +0,0 @@
//
// Article+Database.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/3/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import RSDatabase
import RSDatabaseObjC
import Articles
import RSParser
extension Article {
init?(accountID: String, row: FMResultSet, status: ArticleStatus) {
guard let articleID = row.string(forColumn: DatabaseKey.articleID) else {
assertionFailure("Expected articleID.")
return nil
}
guard let webFeedID = row.string(forColumn: DatabaseKey.feedID) else {
assertionFailure("Expected feedID.")
return nil
}
guard let uniqueID = row.string(forColumn: DatabaseKey.uniqueID) else {
assertionFailure("Expected uniqueID.")
return nil
}
let title = row.string(forColumn: DatabaseKey.title)
let contentHTML = row.string(forColumn: DatabaseKey.contentHTML)
let contentText = row.string(forColumn: DatabaseKey.contentText)
let url = row.string(forColumn: DatabaseKey.url)
let externalURL = row.string(forColumn: DatabaseKey.externalURL)
let summary = row.string(forColumn: DatabaseKey.summary)
let imageURL = row.string(forColumn: DatabaseKey.imageURL)
let datePublished = row.date(forColumn: DatabaseKey.datePublished)
let dateModified = row.date(forColumn: DatabaseKey.dateModified)
self.init(accountID: accountID, articleID: articleID, webFeedID: webFeedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, datePublished: datePublished, dateModified: dateModified, authors: nil, status: status)
}
init(parsedItem: ParsedItem, maximumDateAllowed: Date, accountID: String, webFeedID: String, status: ArticleStatus) {
let authors = Author.authorsWithParsedAuthors(parsedItem.authors)
// Deal with future datePublished and dateModified dates.
var datePublished = parsedItem.datePublished
if datePublished == nil {
datePublished = parsedItem.dateModified
}
if datePublished != nil, datePublished! > maximumDateAllowed {
datePublished = nil
}
var dateModified = parsedItem.dateModified
if dateModified != nil, dateModified! > maximumDateAllowed {
dateModified = nil
}
self.init(accountID: accountID, articleID: parsedItem.syncServiceID, webFeedID: webFeedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, status: status)
}
private func addPossibleStringChangeWithKeyPath(_ comparisonKeyPath: KeyPath<Article,String?>, _ otherArticle: Article, _ key: String, _ dictionary: inout DatabaseDictionary) {
if self[keyPath: comparisonKeyPath] != otherArticle[keyPath: comparisonKeyPath] {
dictionary[key] = self[keyPath: comparisonKeyPath] ?? ""
}
}
func byAdding(_ authors: Set<Author>) -> Article {
if authors.isEmpty {
return self
}
return Article(accountID: self.accountID, articleID: self.articleID, webFeedID: self.webFeedID, uniqueID: self.uniqueID, title: self.title, contentHTML: self.contentHTML, contentText: self.contentText, url: self.url, externalURL: self.externalURL, summary: self.summary, imageURL: self.imageURL, datePublished: self.datePublished, dateModified: self.dateModified, authors: authors, status: self.status)
}
func changesFrom(_ existingArticle: Article) -> DatabaseDictionary? {
if self == existingArticle {
return nil
}
var d = DatabaseDictionary()
if uniqueID != existingArticle.uniqueID {
d[DatabaseKey.uniqueID] = uniqueID
}
addPossibleStringChangeWithKeyPath(\Article.title, existingArticle, DatabaseKey.title, &d)
addPossibleStringChangeWithKeyPath(\Article.contentHTML, existingArticle, DatabaseKey.contentHTML, &d)
addPossibleStringChangeWithKeyPath(\Article.contentText, existingArticle, DatabaseKey.contentText, &d)
addPossibleStringChangeWithKeyPath(\Article.url, existingArticle, DatabaseKey.url, &d)
addPossibleStringChangeWithKeyPath(\Article.externalURL, existingArticle, DatabaseKey.externalURL, &d)
addPossibleStringChangeWithKeyPath(\Article.summary, existingArticle, DatabaseKey.summary, &d)
addPossibleStringChangeWithKeyPath(\Article.imageURL, existingArticle, DatabaseKey.imageURL, &d)
// If updated versions of dates are nil, and we have existing dates, keep the existing dates.
// This is data thats good to have, and its likely that a feed removing dates is doing so in error.
if datePublished != existingArticle.datePublished {
if let updatedDatePublished = datePublished {
d[DatabaseKey.datePublished] = updatedDatePublished
}
}
if dateModified != existingArticle.dateModified {
if let updatedDateModified = dateModified {
d[DatabaseKey.dateModified] = updatedDateModified
}
}
return d.count < 1 ? nil : d
}
// static func articlesWithParsedItems(_ parsedItems: Set<ParsedItem>, _ accountID: String, _ feedID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set<Article> {
// let maximumDateAllowed = Date().addingTimeInterval(60 * 60 * 24) // Allow dates up to about 24 hours ahead of now
// return Set(parsedItems.map{ Article(parsedItem: $0, maximumDateAllowed: maximumDateAllowed, accountID: accountID, feedID: feedID, status: statusesDictionary[$0.articleID]!) })
// }
private static func _maximumDateAllowed() -> Date {
return Date().addingTimeInterval(60 * 60 * 24) // Allow dates up to about 24 hours ahead of now
}
static func articlesWithWebFeedIDsAndItems(_ webFeedIDsAndItems: [String: Set<ParsedItem>], _ accountID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set<Article> {
let maximumDateAllowed = _maximumDateAllowed()
var feedArticles = Set<Article>()
for (webFeedID, parsedItems) in webFeedIDsAndItems {
for parsedItem in parsedItems {
let status = statusesDictionary[parsedItem.articleID]!
let article = Article(parsedItem: parsedItem, maximumDateAllowed: maximumDateAllowed, accountID: accountID, webFeedID: webFeedID, status: status)
feedArticles.insert(article)
}
}
return feedArticles
}
static func articlesWithParsedItems(_ parsedItems: Set<ParsedItem>, _ webFeedID: String, _ accountID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set<Article> {
let maximumDateAllowed = _maximumDateAllowed()
return Set(parsedItems.map{ Article(parsedItem: $0, maximumDateAllowed: maximumDateAllowed, accountID: accountID, webFeedID: webFeedID, status: statusesDictionary[$0.articleID]!) })
}
}
extension Article: DatabaseObject {
public func databaseDictionary() -> DatabaseDictionary? {
var d = DatabaseDictionary()
d[DatabaseKey.articleID] = articleID
d[DatabaseKey.feedID] = webFeedID
d[DatabaseKey.uniqueID] = uniqueID
if let title = title {
d[DatabaseKey.title] = title
}
if let contentHTML = contentHTML {
d[DatabaseKey.contentHTML] = contentHTML
}
if let contentText = contentText {
d[DatabaseKey.contentText] = contentText
}
if let url = url {
d[DatabaseKey.url] = url
}
if let externalURL = externalURL {
d[DatabaseKey.externalURL] = externalURL
}
if let summary = summary {
d[DatabaseKey.summary] = summary
}
if let imageURL = imageURL {
d[DatabaseKey.imageURL] = imageURL
}
if let datePublished = datePublished {
d[DatabaseKey.datePublished] = datePublished
}
if let dateModified = dateModified {
d[DatabaseKey.dateModified] = dateModified
}
return d
}
public var databaseID: String {
return articleID
}
public func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? {
switch name {
case RelationshipName.authors:
return databaseObjectArray(with: authors)
default:
return nil
}
}
private func databaseObjectArray<T: DatabaseObject>(with objects: Set<T>?) -> [DatabaseObject]? {
guard let objects = objects else {
return nil
}
return Array(objects)
}
}
extension Set where Element == Article {
func statuses() -> Set<ArticleStatus> {
return Set<ArticleStatus>(map { $0.status })
}
func dictionary() -> [String: Article] {
var d = [String: Article]()
for article in self {
d[article.articleID] = article
}
return d
}
func databaseObjects() -> [DatabaseObject] {
return self.map{ $0 as DatabaseObject }
}
func databaseDictionaries() -> [DatabaseDictionary]? {
return self.compactMap { $0.databaseDictionary() }
}
}

View File

@@ -1,35 +0,0 @@
//
// ArticleStatus+Database.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/3/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import RSDatabase
import RSDatabaseObjC
import Articles
extension ArticleStatus {
convenience init(articleID: String, dateArrived: Date, row: FMResultSet) {
let read = row.bool(forColumn: DatabaseKey.read)
let starred = row.bool(forColumn: DatabaseKey.starred)
self.init(articleID: articleID, read: read, starred: starred, dateArrived: dateArrived)
}
}
extension ArticleStatus: DatabaseObject {
public var databaseID: String {
return articleID
}
public func databaseDictionary() -> DatabaseDictionary? {
return [DatabaseKey.articleID: articleID, DatabaseKey.read: read, DatabaseKey.starred: starred, DatabaseKey.dateArrived: dateArrived]
}
}

View File

@@ -1,66 +0,0 @@
//
// Author+Database.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/8/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import Articles
import RSDatabase
import RSDatabaseObjC
import RSParser
// MARK: - DatabaseObject
extension Author {
init?(row: FMResultSet) {
let authorID = row.string(forColumn: DatabaseKey.authorID)
let name = row.string(forColumn: DatabaseKey.name)
let url = row.string(forColumn: DatabaseKey.url)
let avatarURL = row.string(forColumn: DatabaseKey.avatarURL)
let emailAddress = row.string(forColumn: DatabaseKey.emailAddress)
self.init(authorID: authorID, name: name, url: url, avatarURL: avatarURL, emailAddress: emailAddress)
}
init?(parsedAuthor: ParsedAuthor) {
self.init(authorID: nil, name: parsedAuthor.name, url: parsedAuthor.url, avatarURL: parsedAuthor.avatarURL, emailAddress: parsedAuthor.emailAddress)
}
public static func authorsWithParsedAuthors(_ parsedAuthors: Set<ParsedAuthor>?) -> Set<Author>? {
guard let parsedAuthors = parsedAuthors else {
return nil
}
let authors = Set(parsedAuthors.compactMap { Author(parsedAuthor: $0) })
return authors.isEmpty ? nil: authors
}
}
extension Author: DatabaseObject {
public var databaseID: String {
return authorID
}
public func databaseDictionary() -> DatabaseDictionary? {
var d: DatabaseDictionary = [DatabaseKey.authorID: authorID]
if let name = name {
d[DatabaseKey.name] = name
}
if let url = url {
d[DatabaseKey.url] = url
}
if let avatarURL = avatarURL {
d[DatabaseKey.avatarURL] = avatarURL
}
if let emailAddress = emailAddress {
d[DatabaseKey.emailAddress] = emailAddress
}
return d
}
}

View File

@@ -1,22 +0,0 @@
//
// ParsedArticle+Database.swift
// NetNewsWire
//
// Created by Brent Simmons on 9/18/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import RSParser
import Articles
extension ParsedItem {
var articleID: String {
if let s = syncServiceID {
return s
}
// Must be same calculation as for Article.
return Article.calculatedArticleID(webFeedID: feedURL, uniqueID: uniqueID)
}
}

View File

@@ -1,75 +0,0 @@
//
// FetchAllUnreadCountsOperation.swift
// ArticlesDatabase
//
// Created by Brent Simmons on 1/26/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import RSCore
import RSDatabase
import RSDatabaseObjC
public final class FetchAllUnreadCountsOperation: MainThreadOperation {
var result: UnreadCountDictionaryCompletionResult = .failure(.isSuspended)
// MainThreadOperation
public var isCanceled = false
public var id: Int?
public weak var operationDelegate: MainThreadOperationDelegate?
public var name: String? = "FetchAllUnreadCountsOperation"
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
private let queue: DatabaseQueue
init(databaseQueue: DatabaseQueue) {
self.queue = databaseQueue
}
public func run() {
queue.runInDatabase { databaseResult in
if self.isCanceled {
self.informOperationDelegateOfCompletion()
return
}
switch databaseResult {
case .success(let database):
self.fetchUnreadCounts(database)
case .failure:
self.informOperationDelegateOfCompletion()
}
}
}
}
private extension FetchAllUnreadCountsOperation {
func fetchUnreadCounts(_ database: FMDatabase) {
let sql = "select distinct feedID, count(*) from articles natural join statuses where read=0 group by feedID;"
guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else {
informOperationDelegateOfCompletion()
return
}
var unreadCountDictionary = UnreadCountDictionary()
while resultSet.next() {
if isCanceled {
resultSet.close()
informOperationDelegateOfCompletion()
return
}
let unreadCount = resultSet.long(forColumnIndex: 1)
if let webFeedID = resultSet.string(forColumnIndex: 0) {
unreadCountDictionary[webFeedID] = unreadCount
}
}
resultSet.close()
result = .success(unreadCountDictionary)
informOperationDelegateOfCompletion()
}
}

View File

@@ -1,75 +0,0 @@
//
// FetchFeedUnreadCountOperation.swift
// ArticlesDatabase
//
// Created by Brent Simmons on 1/27/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import RSCore
import RSDatabase
import RSDatabaseObjC
/// Fetch the unread count for a single feed.
public final class FetchFeedUnreadCountOperation: MainThreadOperation {
var result: SingleUnreadCountResult = .failure(.isSuspended)
// MainThreadOperation
public var isCanceled = false
public var id: Int?
public weak var operationDelegate: MainThreadOperationDelegate?
public var name: String? = "FetchFeedUnreadCountOperation"
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
private let queue: DatabaseQueue
private let cutoffDate: Date
private let webFeedID: String
init(webFeedID: String, databaseQueue: DatabaseQueue, cutoffDate: Date) {
self.webFeedID = webFeedID
self.queue = databaseQueue
self.cutoffDate = cutoffDate
}
public func run() {
queue.runInDatabase { databaseResult in
if self.isCanceled {
self.informOperationDelegateOfCompletion()
return
}
switch databaseResult {
case .success(let database):
self.fetchUnreadCount(database)
case .failure:
self.informOperationDelegateOfCompletion()
}
}
}
}
private extension FetchFeedUnreadCountOperation {
func fetchUnreadCount(_ database: FMDatabase) {
let sql = "select count(*) from articles natural join statuses where feedID=? and read=0;"
guard let resultSet = database.executeQuery(sql, withArgumentsIn: [webFeedID]) else {
informOperationDelegateOfCompletion()
return
}
if isCanceled {
informOperationDelegateOfCompletion()
return
}
if resultSet.next() {
let unreadCount = resultSet.long(forColumnIndex: 0)
result = .success(unreadCount)
}
resultSet.close()
informOperationDelegateOfCompletion()
}
}

View File

@@ -1,86 +0,0 @@
//
// FetchUnreadCountsForFeedsOperation.swift
// ArticlesDatabase
//
// Created by Brent Simmons on 2/1/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import RSCore
import RSDatabase
import RSDatabaseObjC
/// Fetch the unread counts for a number of feeds.
public final class FetchUnreadCountsForFeedsOperation: MainThreadOperation {
var result: UnreadCountDictionaryCompletionResult = .failure(.isSuspended)
// MainThreadOperation
public var isCanceled = false
public var id: Int?
public weak var operationDelegate: MainThreadOperationDelegate?
public var name: String? = "FetchUnreadCountsForFeedsOperation"
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
private let queue: DatabaseQueue
private let webFeedIDs: Set<String>
init(webFeedIDs: Set<String>, databaseQueue: DatabaseQueue) {
self.webFeedIDs = webFeedIDs
self.queue = databaseQueue
}
public func run() {
queue.runInDatabase { databaseResult in
if self.isCanceled {
self.informOperationDelegateOfCompletion()
return
}
switch databaseResult {
case .success(let database):
self.fetchUnreadCounts(database)
case .failure:
self.informOperationDelegateOfCompletion()
}
}
}
}
private extension FetchUnreadCountsForFeedsOperation {
func fetchUnreadCounts(_ database: FMDatabase) {
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
let sql = "select distinct feedID, count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 group by feedID;"
let parameters = Array(webFeedIDs) as [Any]
guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else {
informOperationDelegateOfCompletion()
return
}
if isCanceled {
resultSet.close()
informOperationDelegateOfCompletion()
return
}
var unreadCountDictionary = UnreadCountDictionary()
while resultSet.next() {
if isCanceled {
resultSet.close()
informOperationDelegateOfCompletion()
return
}
let unreadCount = resultSet.long(forColumnIndex: 1)
if let webFeedID = resultSet.string(forColumnIndex: 0) {
unreadCountDictionary[webFeedID] = unreadCount
}
}
resultSet.close()
result = .success(unreadCountDictionary)
informOperationDelegateOfCompletion()
}
}

View File

@@ -1,21 +0,0 @@
//
// RelatedObjectsMap+Database.swift
// Database
//
// Created by Brent Simmons on 9/13/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import RSDatabase
import Articles
extension RelatedObjectsMap {
func authors(for articleID: String) -> Set<Author>? {
if let objects = self[articleID] {
return objects.asAuthors()
}
return nil
}
}

View File

@@ -1,210 +0,0 @@
//
// SearchTable.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/23/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import RSCore
import RSDatabase
import RSDatabaseObjC
import Articles
import RSParser
final class ArticleSearchInfo: Hashable {
let articleID: String
let title: String?
let contentHTML: String?
let contentText: String?
let summary: String?
let searchRowID: Int?
var preferredText: String {
if let body = contentHTML, !body.isEmpty {
return body
}
if let body = contentText, !body.isEmpty {
return body
}
return summary ?? ""
}
lazy var bodyForIndex: String = {
let s = preferredText.rsparser_stringByDecodingHTMLEntities()
return s.strippingHTML().collapsingWhitespace
}()
init(articleID: String, title: String?, contentHTML: String?, contentText: String?, summary: String?, searchRowID: Int?) {
self.articleID = articleID
self.title = title
self.contentHTML = contentHTML
self.contentText = contentText
self.summary = summary
self.searchRowID = searchRowID
}
convenience init(article: Article) {
self.init(articleID: article.articleID, title: article.title, contentHTML: article.contentHTML, contentText: article.contentText, summary: article.summary, searchRowID: nil)
}
// MARK: Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(articleID)
}
// MARK: Equatable
static func == (lhs: ArticleSearchInfo, rhs: ArticleSearchInfo) -> Bool {
return lhs.articleID == rhs.articleID && lhs.title == rhs.title && lhs.contentHTML == rhs.contentHTML && lhs.contentText == rhs.contentText && lhs.summary == rhs.summary && lhs.searchRowID == rhs.searchRowID
}
}
final class SearchTable: DatabaseTable {
let name = "search"
private let queue: DatabaseQueue
private weak var articlesTable: ArticlesTable?
init(queue: DatabaseQueue, articlesTable: ArticlesTable) {
self.queue = queue
self.articlesTable = articlesTable
}
func ensureIndexedArticles(for articleIDs: Set<String>) {
guard !articleIDs.isEmpty else {
return
}
queue.runInTransaction { databaseResult in
if let database = databaseResult.database {
self.ensureIndexedArticles(articleIDs, database)
}
}
}
/// Add to, or update, the search index for articles with specified IDs.
func ensureIndexedArticles(_ articleIDs: Set<String>, _ database: FMDatabase) {
guard let articlesTable = articlesTable else {
return
}
guard let articleSearchInfos = articlesTable.fetchArticleSearchInfos(articleIDs, in: database) else {
return
}
let unindexedArticles = articleSearchInfos.filter { $0.searchRowID == nil }
performInitialIndexForArticles(unindexedArticles, database)
let indexedArticles = articleSearchInfos.filter { $0.searchRowID != nil }
updateIndexForArticles(indexedArticles, database)
}
/// Index new articles.
func indexNewArticles(_ articles: Set<Article>, _ database: FMDatabase) {
let articleSearchInfos = Set(articles.map{ ArticleSearchInfo(article: $0) })
performInitialIndexForArticles(articleSearchInfos, database)
}
/// Index updated articles.
func indexUpdatedArticles(_ articles: Set<Article>, _ database: FMDatabase) {
ensureIndexedArticles(articles.articleIDs(), database)
}
}
// MARK: - Private
private extension SearchTable {
func performInitialIndexForArticles(_ articles: Set<ArticleSearchInfo>, _ database: FMDatabase) {
articles.forEach { performInitialIndex($0, database) }
}
func performInitialIndex(_ article: ArticleSearchInfo, _ database: FMDatabase) {
let rowid = insert(article, database)
articlesTable?.updateRowsWithValue(rowid, valueKey: DatabaseKey.searchRowID, whereKey: DatabaseKey.articleID, matches: [article.articleID], database: database)
}
func insert(_ article: ArticleSearchInfo, _ database: FMDatabase) -> Int {
let rowDictionary: DatabaseDictionary = [DatabaseKey.body: article.bodyForIndex, DatabaseKey.title: article.title ?? ""]
insertRow(rowDictionary, insertType: .normal, in: database)
return Int(database.lastInsertRowId())
}
private struct SearchInfo: Hashable {
let rowID: Int
let title: String
let body: String
init(row: FMResultSet) {
self.rowID = Int(row.longLongInt(forColumn: DatabaseKey.rowID))
self.title = row.string(forColumn: DatabaseKey.title) ?? ""
self.body = row.string(forColumn: DatabaseKey.body) ?? ""
}
// MARK: Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(rowID)
}
}
func updateIndexForArticles(_ articles: Set<ArticleSearchInfo>, _ database: FMDatabase) {
if articles.isEmpty {
return
}
guard let searchInfos = fetchSearchInfos(articles, database) else {
// The articles that get here have a non-nil searchRowID, and we should have found rows in the search table for them.
// But we didnt. Recover by doing an initial index.
performInitialIndexForArticles(articles, database)
return
}
let groupedSearchInfos = Dictionary(grouping: searchInfos, by: { $0.rowID })
let searchInfosDictionary = groupedSearchInfos.mapValues { $0.first! }
articles.forEach { (article) in
updateIndexForArticle(article, searchInfosDictionary, database)
}
}
private func updateIndexForArticle(_ article: ArticleSearchInfo, _ searchInfosDictionary: [Int: SearchInfo], _ database: FMDatabase) {
guard let searchRowID = article.searchRowID else {
assertionFailure("Expected article.searchRowID, got nil")
return
}
guard let searchInfo: SearchInfo = searchInfosDictionary[searchRowID] else {
// Shouldnt happen. The article has a searchRowID, but we didnt find that row in the search table.
// Easy to recover from: just do an initial index, and alls well.
performInitialIndex(article, database)
return
}
let title = article.title ?? ""
if title == searchInfo.title && article.bodyForIndex == searchInfo.body {
return
}
var updateDictionary = DatabaseDictionary()
if title != searchInfo.title {
updateDictionary[DatabaseKey.title] = title
}
if article.bodyForIndex != searchInfo.body {
updateDictionary[DatabaseKey.body] = article.bodyForIndex
}
updateRowsWithDictionary(updateDictionary, whereKey: DatabaseKey.rowID, matches: searchInfo.rowID, database: database)
}
private func fetchSearchInfos(_ articles: Set<ArticleSearchInfo>, _ database: FMDatabase) -> Set<SearchInfo>? {
let searchRowIDs = articles.compactMap { $0.searchRowID }
guard !searchRowIDs.isEmpty else {
return nil
}
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(searchRowIDs.count))!
let sql = "select rowid, title, body from \(name) where rowid in \(placeholders);"
guard let resultSet = database.executeQuery(sql, withArgumentsIn: searchRowIDs) else {
return nil
}
return resultSet.mapToSet { SearchInfo(row: $0) }
}
}

View File

@@ -1,289 +0,0 @@
//
// StatusesTable.swift
// NetNewsWire
//
// Created by Brent Simmons on 5/8/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import RSDatabase
import RSDatabaseObjC
import Articles
// Article->ArticleStatus is a to-one relationship.
//
// CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0);
final class StatusesTable: DatabaseTable {
let name = DatabaseTableName.statuses
private let cache = StatusCache()
private let queue: DatabaseQueue
init(queue: DatabaseQueue) {
self.queue = queue
}
// MARK: - Creating/Updating
func ensureStatusesForArticleIDs(_ articleIDs: Set<String>, _ read: Bool, _ database: FMDatabase) -> ([String: ArticleStatus], Set<String>) {
// Check cache.
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs)
if articleIDsMissingCachedStatus.isEmpty {
return (statusesDictionary(articleIDs), Set<String>())
}
// Check database.
fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus, database)
let articleIDsNeedingStatus = self.articleIDsWithNoCachedStatus(articleIDs)
if !articleIDsNeedingStatus.isEmpty {
// Create new statuses.
self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus, read, database)
}
return (statusesDictionary(articleIDs), articleIDsNeedingStatus)
}
func existingStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) -> [String: ArticleStatus] {
// Check cache.
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs)
if articleIDsMissingCachedStatus.isEmpty {
return statusesDictionary(articleIDs)
}
// Check database.
fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus, database)
return statusesDictionary(articleIDs)
}
// MARK: - Marking
@discardableResult
func mark(_ statuses: Set<ArticleStatus>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) -> Set<ArticleStatus>? {
// Sets flag in both memory and in database.
var updatedStatuses = Set<ArticleStatus>()
for status in statuses {
if status.boolStatus(forKey: statusKey) == flag {
continue
}
status.setBoolStatus(flag, forKey: statusKey)
updatedStatuses.insert(status)
}
if updatedStatuses.isEmpty {
return nil
}
let articleIDs = updatedStatuses.articleIDs()
self.markArticleIDs(articleIDs, statusKey, flag, database)
return updatedStatuses
}
func markAndFetchNew(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) -> Set<String> {
let (statusesDictionary, newStatusIDs) = ensureStatusesForArticleIDs(articleIDs, flag, database)
let statuses = Set(statusesDictionary.values)
mark(statuses, statusKey, flag, database)
return newStatusIDs
}
// MARK: - Fetching
func fetchUnreadArticleIDs() throws -> Set<String> {
return try fetchArticleIDs("select articleID from statuses where read=0;")
}
func fetchStarredArticleIDs() throws -> Set<String> {
return try fetchArticleIDs("select articleID from statuses where starred=1;")
}
func fetchArticleIDsForStatusesWithoutArticlesNewerThan(_ cutoffDate: Date, _ completion: @escaping ArticleIDsCompletionBlock) {
queue.runInDatabase { databaseResult in
var error: DatabaseError?
var articleIDs = Set<String>()
func makeDatabaseCall(_ database: FMDatabase) {
let sql = "select articleID from statuses s where (starred=1 or dateArrived>?) and not exists (select 1 from articles a where a.articleID = s.articleID);"
if let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) {
articleIDs = resultSet.mapToSet(self.articleIDWithRow)
}
}
switch databaseResult {
case .success(let database):
makeDatabaseCall(database)
case .failure(let databaseError):
error = databaseError
}
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
}
else {
DispatchQueue.main.async {
completion(.success(articleIDs))
}
}
}
}
func fetchArticleIDs(_ sql: String) throws -> Set<String> {
var error: DatabaseError?
var articleIDs = Set<String>()
queue.runInDatabaseSync { databaseResult in
switch databaseResult {
case .success(let database):
if let resultSet = database.executeQuery(sql, withArgumentsIn: nil) {
articleIDs = resultSet.mapToSet(self.articleIDWithRow)
}
case .failure(let databaseError):
error = databaseError
}
}
if let error = error {
throw(error)
}
return articleIDs
}
func articleIDWithRow(_ row: FMResultSet) -> String? {
return row.string(forColumn: DatabaseKey.articleID)
}
func statusWithRow(_ row: FMResultSet) -> ArticleStatus? {
guard let articleID = row.string(forColumn: DatabaseKey.articleID) else {
return nil
}
return statusWithRow(row, articleID: articleID)
}
func statusWithRow(_ row: FMResultSet, articleID: String) ->ArticleStatus? {
if let cachedStatus = cache[articleID] {
return cachedStatus
}
guard let dateArrived = row.date(forColumn: DatabaseKey.dateArrived) else {
return nil
}
let articleStatus = ArticleStatus(articleID: articleID, dateArrived: dateArrived, row: row)
cache.addStatusIfNotCached(articleStatus)
return articleStatus
}
func statusesDictionary(_ articleIDs: Set<String>) -> [String: ArticleStatus] {
var d = [String: ArticleStatus]()
for articleID in articleIDs {
if let articleStatus = cache[articleID] {
d[articleID] = articleStatus
}
}
return d
}
// MARK: - Cleanup
func removeStatuses(_ articleIDs: Set<String>, _ database: FMDatabase) {
deleteRowsWhere(key: DatabaseKey.articleID, equalsAnyValue: Array(articleIDs), in: database)
}
}
// MARK: - Private
private extension StatusesTable {
// MARK: - Cache
func articleIDsWithNoCachedStatus(_ articleIDs: Set<String>) -> Set<String> {
return Set(articleIDs.filter { cache[$0] == nil })
}
// MARK: - Creating
func saveStatuses(_ statuses: Set<ArticleStatus>, _ database: FMDatabase) {
let statusArray = statuses.map { $0.databaseDictionary()! }
self.insertRows(statusArray, insertType: .orIgnore, in: database)
}
func createAndSaveStatusesForArticleIDs(_ articleIDs: Set<String>, _ read: Bool, _ database: FMDatabase) {
let now = Date()
let statuses = Set(articleIDs.map { ArticleStatus(articleID: $0, read: read, dateArrived: now) })
cache.addIfNotCached(statuses)
saveStatuses(statuses, database)
}
func fetchAndCacheStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) {
guard let resultSet = self.selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) else {
return
}
let statuses = resultSet.mapToSet(self.statusWithRow)
self.cache.addIfNotCached(statuses)
}
// MARK: - Marking
func markArticleIDs(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) {
updateRowsWithValue(NSNumber(value: flag), valueKey: statusKey.rawValue, whereKey: DatabaseKey.articleID, matches: Array(articleIDs), database: database)
}
}
// MARK: -
private final class StatusCache {
// Serial database queue only.
var dictionary = [String: ArticleStatus]()
var cachedStatuses: Set<ArticleStatus> {
return Set(dictionary.values)
}
func add(_ statuses: Set<ArticleStatus>) {
// Replaces any cached statuses.
for status in statuses {
self[status.articleID] = status
}
}
func addStatusIfNotCached(_ status: ArticleStatus) {
addIfNotCached(Set([status]))
}
func addIfNotCached(_ statuses: Set<ArticleStatus>) {
// Does not replace already cached statuses.
for status in statuses {
let articleID = status.articleID
if let _ = self[articleID] {
continue
}
self[articleID] = status
}
}
subscript(_ articleID: String) -> ArticleStatus? {
get {
return dictionary[articleID]
}
set {
dictionary[articleID] = newValue
}
}
}

View File

@@ -1,25 +0,0 @@
//
// Constants.swift
// SyncDatabase
//
// Created by Maurice Parker on 5/14/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
struct DatabaseTableName {
static let syncStatus = "syncStatus"
}
struct DatabaseKey {
// Sync Status
static let articleID = "articleID"
static let key = "key"
static let flag = "flag"
static let selected = "selected"
}

View File

@@ -1,92 +0,0 @@
//
// SyncDatabase.swift
// NetNewsWire
//
// Created by Maurice Parker on 5/14/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import RSCore
import RSDatabase
public typealias SyncStatusesResult = Result<Array<SyncStatus>, DatabaseError>
public typealias SyncStatusesCompletionBlock = (SyncStatusesResult) -> Void
public typealias SyncStatusArticleIDsResult = Result<Set<String>, DatabaseError>
public typealias SyncStatusArticleIDsCompletionBlock = (SyncStatusArticleIDsResult) -> Void
public struct SyncDatabase {
private let syncStatusTable: SyncStatusTable
private let queue: DatabaseQueue
public init(databaseFilePath: String) {
let queue = DatabaseQueue(databasePath: databaseFilePath)
try! queue.runCreateStatements(SyncDatabase.tableCreationStatements)
queue.vacuumIfNeeded(daysBetweenVacuums: 11)
self.queue = queue
self.syncStatusTable = SyncStatusTable(queue: queue)
}
// MARK: - API
public func insertStatuses(_ statuses: [SyncStatus]) throws {
try syncStatusTable.insertStatuses(statuses)
}
public func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) {
syncStatusTable.insertStatuses(statuses, completion: completion)
}
public func selectForProcessing(limit: Int? = nil, completion: @escaping SyncStatusesCompletionBlock) {
return syncStatusTable.selectForProcessing(limit: limit, completion: completion)
}
public func selectPendingCount(completion: @escaping DatabaseIntCompletionBlock) {
syncStatusTable.selectPendingCount(completion)
}
public func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
syncStatusTable.selectPendingReadStatusArticleIDs(completion: completion)
}
public func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
syncStatusTable.selectPendingStarredStatusArticleIDs(completion: completion)
}
public func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) {
syncStatusTable.resetAllSelectedForProcessing(completion: completion)
}
public func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) {
syncStatusTable.resetSelectedForProcessing(articleIDs, completion: completion)
}
public func deleteSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) {
syncStatusTable.deleteSelectedForProcessing(articleIDs, completion: completion)
}
// MARK: - Suspend and Resume (for iOS)
/// Close the database and stop running database calls.
/// Any pending calls will complete first.
public func suspend() {
queue.suspend()
}
/// Open the database and allow for running database calls again.
public func resume() {
queue.resume()
}
}
// MARK: - Private
private extension SyncDatabase {
static let tableCreationStatements = """
CREATE TABLE if not EXISTS syncStatus (articleID TEXT NOT NULL, key TEXT NOT NULL, flag BOOL NOT NULL DEFAULT 0, selected BOOL NOT NULL DEFAULT 0, PRIMARY KEY (articleID, key));
"""
}

View File

@@ -1,298 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
51554C15228B6F0D0055115A /* SyncStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51554C12228B6F0D0055115A /* SyncStatus.swift */; };
51554C16228B6F0D0055115A /* SyncStatusTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51554C13228B6F0D0055115A /* SyncStatusTable.swift */; };
51554C17228B6F0D0055115A /* SyncDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51554C14228B6F0D0055115A /* SyncDatabase.swift */; };
51554C1E228B701F0055115A /* SyncDatabase_project.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 51554C19228B701F0055115A /* SyncDatabase_project.xcconfig */; };
51554C1F228B701F0055115A /* SyncDatabase_project_debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 51554C1A228B701F0055115A /* SyncDatabase_project_debug.xcconfig */; };
51554C21228B701F0055115A /* SyncDatabase_project_release.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 51554C1C228B701F0055115A /* SyncDatabase_project_release.xcconfig */; };
51554C22228B701F0055115A /* SyncDatabase_target.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 51554C1D228B701F0055115A /* SyncDatabase_target.xcconfig */; };
51554C38228B7DAC0055115A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51554C37228B7DAC0055115A /* Constants.swift */; };
51554C3A228B83380055115A /* Articles.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C39228B83380055115A /* Articles.framework */; };
51B0DF1324D24EA6000AD99E /* RSDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 51B0DF1224D24EA6000AD99E /* RSDatabase */; };
51B0DF1424D24EA6000AD99E /* RSDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51B0DF1224D24EA6000AD99E /* RSDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
51B0DEC524D245A2000AD99E /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
51B0DF1424D24EA6000AD99E /* RSDatabase in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
51554BEB228B6E8F0055115A /* SyncDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SyncDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; };
51554BEF228B6E8F0055115A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
51554C12228B6F0D0055115A /* SyncStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncStatus.swift; sourceTree = "<group>"; };
51554C13228B6F0D0055115A /* SyncStatusTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncStatusTable.swift; sourceTree = "<group>"; };
51554C14228B6F0D0055115A /* SyncDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncDatabase.swift; sourceTree = "<group>"; };
51554C19228B701F0055115A /* SyncDatabase_project.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = SyncDatabase_project.xcconfig; sourceTree = "<group>"; };
51554C1A228B701F0055115A /* SyncDatabase_project_debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = SyncDatabase_project_debug.xcconfig; sourceTree = "<group>"; };
51554C1C228B701F0055115A /* SyncDatabase_project_release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = SyncDatabase_project_release.xcconfig; sourceTree = "<group>"; };
51554C1D228B701F0055115A /* SyncDatabase_target.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = SyncDatabase_target.xcconfig; sourceTree = "<group>"; };
51554C35228B72F40055115A /* RSDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; };
51554C37228B7DAC0055115A /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
51554C39228B83380055115A /* Articles.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Articles.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
51554BE8228B6E8F0055115A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
51B0DF1324D24EA6000AD99E /* RSDatabase in Frameworks */,
51554C3A228B83380055115A /* Articles.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
51554BE1228B6E8F0055115A = {
isa = PBXGroup;
children = (
51554C37228B7DAC0055115A /* Constants.swift */,
51554C14228B6F0D0055115A /* SyncDatabase.swift */,
51554C12228B6F0D0055115A /* SyncStatus.swift */,
51554C13228B6F0D0055115A /* SyncStatusTable.swift */,
51554BEF228B6E8F0055115A /* Info.plist */,
51554BEC228B6E8F0055115A /* Products */,
51554C18228B6FBE0055115A /* xcconfig */,
51554C34228B72F40055115A /* Frameworks */,
);
sourceTree = "<group>";
};
51554BEC228B6E8F0055115A /* Products */ = {
isa = PBXGroup;
children = (
51554BEB228B6E8F0055115A /* SyncDatabase.framework */,
);
name = Products;
sourceTree = "<group>";
};
51554C18228B6FBE0055115A /* xcconfig */ = {
isa = PBXGroup;
children = (
51554C1A228B701F0055115A /* SyncDatabase_project_debug.xcconfig */,
51554C1C228B701F0055115A /* SyncDatabase_project_release.xcconfig */,
51554C19228B701F0055115A /* SyncDatabase_project.xcconfig */,
51554C1D228B701F0055115A /* SyncDatabase_target.xcconfig */,
);
path = xcconfig;
sourceTree = "<group>";
};
51554C34228B72F40055115A /* Frameworks */ = {
isa = PBXGroup;
children = (
51554C39228B83380055115A /* Articles.framework */,
51554C35228B72F40055115A /* RSDatabase.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
51554BE6228B6E8F0055115A /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
51554BEA228B6E8F0055115A /* SyncDatabase */ = {
isa = PBXNativeTarget;
buildConfigurationList = 51554BF3228B6E8F0055115A /* Build configuration list for PBXNativeTarget "SyncDatabase" */;
buildPhases = (
51554BE6228B6E8F0055115A /* Headers */,
51554BE7228B6E8F0055115A /* Sources */,
51554BE8228B6E8F0055115A /* Frameworks */,
51554BE9228B6E8F0055115A /* Resources */,
51C8F349234FB0C40048ED95 /* Run Script: Verfiy No Build Settings */,
51B0DEC524D245A2000AD99E /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = SyncDatabase;
packageProductDependencies = (
51B0DF1224D24EA6000AD99E /* RSDatabase */,
);
productName = SyncDatabase;
productReference = 51554BEB228B6E8F0055115A /* SyncDatabase.framework */;
productType = "com.apple.product-type.framework";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
51554BE2228B6E8F0055115A /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "Ranchero Software";
TargetAttributes = {
51554BEA228B6E8F0055115A = {
CreatedOnToolsVersion = 10.2.1;
};
};
};
buildConfigurationList = 51554BE5228B6E8F0055115A /* Build configuration list for PBXProject "SyncDatabase" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
);
mainGroup = 51554BE1228B6E8F0055115A;
packageReferences = (
51B0DF1124D24EA6000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */,
);
productRefGroup = 51554BEC228B6E8F0055115A /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
51554BEA228B6E8F0055115A /* SyncDatabase */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
51554BE9228B6E8F0055115A /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
51554C21228B701F0055115A /* SyncDatabase_project_release.xcconfig in Resources */,
51554C22228B701F0055115A /* SyncDatabase_target.xcconfig in Resources */,
51554C1F228B701F0055115A /* SyncDatabase_project_debug.xcconfig in Resources */,
51554C1E228B701F0055115A /* SyncDatabase_project.xcconfig in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
51C8F349234FB0C40048ED95 /* Run Script: Verfiy No Build Settings */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Run Script: Verfiy No Build Settings";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [ \"$ENABLE_PREVIEWS\" = \"YES\" ]; then exit 0; fi\n\nxcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n\nif [ $? -ne 0 ]\nthen\n echo \"error: Build Setting were found in the project.pbxproj file. Most likely you didn't intend to change this file and should revert it.\"\n exit 1\nfi\n\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
51554BE7228B6E8F0055115A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
51554C15228B6F0D0055115A /* SyncStatus.swift in Sources */,
51554C16228B6F0D0055115A /* SyncStatusTable.swift in Sources */,
51554C17228B6F0D0055115A /* SyncDatabase.swift in Sources */,
51554C38228B7DAC0055115A /* Constants.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
51554BF1228B6E8F0055115A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 51554C1A228B701F0055115A /* SyncDatabase_project_debug.xcconfig */;
buildSettings = {
};
name = Debug;
};
51554BF2228B6E8F0055115A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 51554C1C228B701F0055115A /* SyncDatabase_project_release.xcconfig */;
buildSettings = {
};
name = Release;
};
51554BF4228B6E8F0055115A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 51554C1D228B701F0055115A /* SyncDatabase_target.xcconfig */;
buildSettings = {
};
name = Debug;
};
51554BF5228B6E8F0055115A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 51554C1D228B701F0055115A /* SyncDatabase_target.xcconfig */;
buildSettings = {
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
51554BE5228B6E8F0055115A /* Build configuration list for PBXProject "SyncDatabase" */ = {
isa = XCConfigurationList;
buildConfigurations = (
51554BF1228B6E8F0055115A /* Debug */,
51554BF2228B6E8F0055115A /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
51554BF3228B6E8F0055115A /* Build configuration list for PBXNativeTarget "SyncDatabase" */ = {
isa = XCConfigurationList;
buildConfigurations = (
51554BF4228B6E8F0055115A /* Debug */,
51554BF5228B6E8F0055115A /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
51B0DF1124D24EA6000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Ranchero-Software/RSDatabase.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = "1.0.0-beta1";
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
51B0DF1224D24EA6000AD99E /* RSDatabase */ = {
isa = XCSwiftPackageProductDependency;
package = 51B0DF1124D24EA6000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */;
productName = RSDatabase;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 51554BE2228B6E8F0055115A /* Project object */;
}

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:SyncDatabase.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -1,48 +0,0 @@
//
// SyncStatus.swift
// NetNewsWire
//
// Created by Maurice Parker on 5/14/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import Articles
import RSDatabase
public struct SyncStatus: Hashable, Equatable {
public enum Key: String {
case read = "read"
case starred = "starred"
case deleted = "deleted"
case new = "new"
public init(_ articleStatusKey: ArticleStatus.Key) {
switch articleStatusKey {
case .read:
self = Self.read
case .starred:
self = Self.starred
}
}
}
public let articleID: String
public let key: SyncStatus.Key
public let flag: Bool
public let selected: Bool
public init(articleID: String, key: SyncStatus.Key, flag: Bool, selected: Bool = false) {
self.articleID = articleID
self.key = key
self.flag = flag
self.selected = selected
}
public func databaseDictionary() -> DatabaseDictionary {
return [DatabaseKey.articleID: articleID, DatabaseKey.key: key.rawValue, DatabaseKey.flag: flag, DatabaseKey.selected: selected]
}
}

View File

@@ -1,248 +0,0 @@
//
// SyncStatusTable.swift
// NetNewsWire
//
// Created by Maurice Parker on 5/14/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import RSCore
import Articles
import RSDatabase
import RSDatabaseObjC
struct SyncStatusTable: DatabaseTable {
let name = DatabaseTableName.syncStatus
private let queue: DatabaseQueue
init(queue: DatabaseQueue) {
self.queue = queue
}
func selectForProcessing(limit: Int?, completion: @escaping SyncStatusesCompletionBlock) {
queue.runInTransaction { databaseResult in
var statuses = Set<SyncStatus>()
var error: DatabaseError?
func makeDatabaseCall(_ database: FMDatabase) {
let updateSQL = "update syncStatus set selected = true"
database.executeUpdate(updateSQL, withArgumentsIn: nil)
var selectSQL = "select * from syncStatus where selected == true"
if let limit = limit {
selectSQL = "\(selectSQL) limit \(limit)"
}
if let resultSet = database.executeQuery(selectSQL, withArgumentsIn: nil) {
statuses = resultSet.mapToSet(self.statusWithRow)
}
}
switch databaseResult {
case .success(let database):
makeDatabaseCall(database)
case .failure(let databaseError):
error = databaseError
}
DispatchQueue.main.async {
if let error = error {
completion(.failure(error))
}
else {
completion(.success(Array(statuses)))
}
}
}
}
func selectPendingCount(_ completion: @escaping DatabaseIntCompletionBlock) {
queue.runInDatabase { databaseResult in
var count: Int = 0
var error: DatabaseError?
func makeDatabaseCall(_ database: FMDatabase) {
let sql = "select count(*) from syncStatus"
if let resultSet = database.executeQuery(sql, withArgumentsIn: nil) {
count = self.numberWithCountResultSet(resultSet)
}
}
switch databaseResult {
case .success(let database):
makeDatabaseCall(database)
case .failure(let databaseError):
error = databaseError
}
DispatchQueue.main.async {
if let error = error {
completion(.failure(error))
}
else {
completion(.success(count))
}
}
}
}
func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
selectPendingArticleIDsAsync(.read, completion)
}
func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
selectPendingArticleIDsAsync(.starred, completion)
}
func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) {
queue.runInTransaction { databaseResult in
func makeDatabaseCall(_ database: FMDatabase) {
let updateSQL = "update syncStatus set selected = false"
database.executeUpdate(updateSQL, withArgumentsIn: nil)
}
switch databaseResult {
case .success(let database):
makeDatabaseCall(database)
callCompletion(completion, nil)
case .failure(let databaseError):
callCompletion(completion, databaseError)
}
}
}
func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) {
queue.runInTransaction { databaseResult in
func makeDatabaseCall(_ database: FMDatabase) {
let parameters = articleIDs.map { $0 as AnyObject }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))!
let updateSQL = "update syncStatus set selected = false where articleID in \(placeholders)"
database.executeUpdate(updateSQL, withArgumentsIn: parameters)
}
switch databaseResult {
case .success(let database):
makeDatabaseCall(database)
callCompletion(completion, nil)
case .failure(let databaseError):
callCompletion(completion, databaseError)
}
}
}
func deleteSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) {
queue.runInTransaction { databaseResult in
func makeDatabaseCall(_ database: FMDatabase) {
let parameters = articleIDs.map { $0 as AnyObject }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))!
let deleteSQL = "delete from syncStatus where selected = true and articleID in \(placeholders)"
database.executeUpdate(deleteSQL, withArgumentsIn: parameters)
}
switch databaseResult {
case .success(let database):
makeDatabaseCall(database)
callCompletion(completion, nil)
case .failure(let databaseError):
callCompletion(completion, databaseError)
}
}
}
func insertStatuses(_ statuses: [SyncStatus]) throws {
var error: DatabaseError?
queue.runInTransactionSync { databaseResult in
switch databaseResult {
case .success(let database):
let statusArray = statuses.map { $0.databaseDictionary() }
self.insertRows(statusArray, insertType: .orReplace, in: database)
case .failure(let databaseError):
error = databaseError
}
}
if let error = error {
throw error
}
}
func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) {
queue.runInTransaction { databaseResult in
func makeDatabaseCall(_ database: FMDatabase) {
let statusArray = statuses.map { $0.databaseDictionary() }
self.insertRows(statusArray, insertType: .orReplace, in: database)
}
switch databaseResult {
case .success(let database):
makeDatabaseCall(database)
completion(nil)
case .failure(let databaseError):
completion(databaseError)
}
}
}
}
private extension SyncStatusTable {
func statusWithRow(_ row: FMResultSet) -> SyncStatus? {
guard let articleID = row.string(forColumn: DatabaseKey.articleID),
let rawKey = row.string(forColumn: DatabaseKey.key),
let key = SyncStatus.Key(rawValue: rawKey) else {
return nil
}
let flag = row.bool(forColumn: DatabaseKey.flag)
let selected = row.bool(forColumn: DatabaseKey.selected)
return SyncStatus(articleID: articleID, key: key, flag: flag, selected: selected)
}
func selectPendingArticleIDsAsync(_ statusKey: ArticleStatus.Key, _ completion: @escaping SyncStatusArticleIDsCompletionBlock) {
queue.runInDatabase { databaseResult in
func makeDatabaseCall(_ database: FMDatabase) {
let sql = "select articleID from syncStatus where selected == false and key = \"\(statusKey.rawValue)\";"
guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else {
DispatchQueue.main.async {
completion(.success(Set<String>()))
}
return
}
let articleIDs = resultSet.mapToSet{ $0.string(forColumnIndex: 0) }
DispatchQueue.main.async {
completion(.success(articleIDs))
}
}
switch databaseResult {
case .success(let database):
makeDatabaseCall(database)
case .failure(let databaseError):
DispatchQueue.main.async {
completion(.failure(databaseError))
}
}
}
}
}
private func callCompletion(_ completion: DatabaseCompletionBlock?, _ databaseError: DatabaseError?) {
guard let completion = completion else {
return
}
DispatchQueue.main.async {
completion(databaseError)
}
}