Commit 13149b10 authored by Chaitanya Pandit's avatar Chaitanya Pandit Committed by Amirjanyan
Browse files

Create Auth UI flow

parent 40592674
......@@ -30,6 +30,7 @@
545A24AE2639ECAB00E0CC12 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545A24AD2639ECAA00E0CC12 /* Authentication.swift */; };
545A24B62639EE2F00E0CC12 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545A24B52639EE2F00E0CC12 /* Data.swift */; };
545FD29726205F1200F23F46 /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545FD29626205F1200F23F46 /* Dictionary.swift */; };
547780D5267A6545009DC45B /* PluginHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547780D4267A6545009DC45B /* PluginHandler.swift */; };
54663245267B767F009D9B46 /* CustomRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54663244267B767F009D9B46 /* CustomRenderer.swift */; };
54663275267BDE56009D9B46 /* SettingsPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54663274267BDE56009D9B46 /* SettingsPane.swift */; };
5478EDD92641A3FC000EB938 /* ItemRecord+Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5478EDD82641A3FC000EB938 /* ItemRecord+Sync.swift */; };
......@@ -268,6 +269,7 @@
545A24AD2639ECAA00E0CC12 /* Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authentication.swift; sourceTree = "<group>"; };
545A24B52639EE2F00E0CC12 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
545FD29626205F1200F23F46 /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = "<group>"; };
547780D4267A6545009DC45B /* PluginHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginHandler.swift; sourceTree = "<group>"; };
54663244267B767F009D9B46 /* CustomRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomRenderer.swift; sourceTree = "<group>"; };
54663274267BDE56009D9B46 /* SettingsPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPane.swift; sourceTree = "<group>"; };
5478EDD82641A3FC000EB938 /* ItemRecord+Sync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ItemRecord+Sync.swift"; sourceTree = "<group>"; };
......@@ -530,6 +532,14 @@
path = views;
sourceTree = "<group>";
};
547780D3267A64E6009DC45B /* Plugins */ = {
isa = PBXGroup;
children = (
547780D4267A6545009DC45B /* PluginHandler.swift */,
);
path = Plugins;
sourceTree = "<group>";
};
54D36B90266E6B97003788F5 /* PubSub */ = {
isa = PBXGroup;
children = (
......@@ -918,6 +928,7 @@
D744F73C25BACB730002C8B7 /* NoteEditorRenderer.swift */,
D7CC3A0E25CE208B001EF34A /* GeneralEditorRenderer.swift */,
D744F74525BADE4A0002C8B7 /* LabelAnnotationRenderer.swift */,
547780DC267A734D009DC45B /* CustomRenderer.swift */,
);
path = Renderers;
sourceTree = "<group>";
......@@ -1071,6 +1082,7 @@
D7EBBBDE2586CD0800A343E6 /* Controllers */ = {
isa = PBXGroup;
children = (
547780D3267A64E6009DC45B /* Plugins */,
D7EBBBDF2586CD1C00A343E6 /* AppController.swift */,
D7EBBC2A2586D1D800A343E6 /* SceneController.swift */,
D7E89C6725A729D9003AEFD8 /* FileStorageController.swift */,
......@@ -1496,6 +1508,7 @@
5430C73826283DFD001F49F1 /* RealRecord.swift in Sources */,
D7E89CF125A867F3003AEFD8 /* CVUAction.swift in Sources */,
D7EBBC212586D05F00A343E6 /* UIHostingControllerNoSafeArea.swift in Sources */,
547780D5267A6545009DC45B /* PluginHandler.swift in Sources */,
D7EBBC162586CFD200A343E6 /* KeyboardToolbar.swift in Sources */,
D7EBBBF92586CE8C00A343E6 /* ViewContext.swift in Sources */,
D749E32025BC40B000E51A83 /* CVU_Button.swift in Sources */,
......
.auth-view {
title: "Login"
defaultRenderer: generalEditor
showContextualBottomBar: false
showBottomBar: false
showDefaultLayout: false
[renderer = generalEditor] {
layout: [
{ section: username, fields: identifier, exclude: labels }
{ section: password, fields: secret, exclude: labels }
{ section: oAuthCode, fields: code, exclude: labels }
{ section: login }
]
}
login {
showTitle: false
HStack {
alignment: center
Spacer
Button {
padding: 20 0 20 0
onPress: [sync back]
VStack {
background: #218721
cornerRadius: 5
Text {
text: "Login"
font: 16 semibold
color: #fff
padding: 5 8 5 8
}
}
}
Spacer
}
}
}
......@@ -1415,6 +1415,7 @@
}
]
},
{
"_type":"Label",
"id":1000,
......@@ -3195,6 +3196,26 @@
}
]
},
{
"_type":"Plugin",
"name":"Template",
"container":"plugin_template",
"icon":"message",
"itemDescription":"Template Plugin by Alp",
"allEdges":[
{
"_type":"view",
"targetType":"CVUStoredDefinition",
"id":202106162000
}
]
},
{
"_type":"CVUStoredDefinition",
"id":202106162000,
"definition":"auth-view"
},
{
"_type": "Post",
"message": "OMG. I love @TheSuffers so much! When I need some inspiration I just listen. https://www.youtube.com/watch?v=RM3hj-6F1pk",
......
......@@ -45,6 +45,21 @@
"property": "service",
"value_type": "string"
},
{
"item_type": "Account",
"property": "identifier",
"value_type": "string"
},
{
"item_type": "Account",
"property": "secret",
"value_type": "string"
},
{
"item_type": "Account",
"property": "code",
"value_type": "string"
},
{
"item_type": "Account",
"property": "itemType",
......@@ -1684,6 +1699,16 @@
"property": "container",
"value_type": "string"
},
{
"item_type": "StartPlugin",
"property": "state",
"value_type": "string"
},
{
"item_type": "StartPlugin",
"property": "oAuthUrl",
"value_type": "string"
},
{
"item_type": "StartPlugin",
"property": "targetItemId",
......
......@@ -126,6 +126,8 @@ func cvuAction(named: String) -> CVUAction.Type? {
return CVUAction_ToPreviousItem.self
case "selectall":
return CVUAction_SelectAll.self
case "sync":
return CVUAction_Sync.self
case "deselectall":
return CVUAction_DeselectAll.self
default:
......@@ -400,7 +402,8 @@ struct CVUAction_StartPlugin: CVUAction {
let lookup = CVULookupController()
let db = sceneController.appController.databaseController
guard let targetItemIdValue = vars["targetItemId"],
guard let plugin = context.currentItem,
let targetItemIdValue = vars["targetItemId"],
let targetItemId: String = lookup.resolve(value: targetItemIdValue, context: context, db: db),
let containerValue = vars["container"],
let container: String = lookup.resolve(value: containerValue, context: context, db: db) else {
......@@ -410,12 +413,59 @@ struct CVUAction_StartPlugin: CVUAction {
do {
let startPluginItem = ItemRecord(type: "StartPlugin")
try startPluginItem.save()
try startPluginItem.setPropertyValue(name: "container", value: .string(container))
try startPluginItem.setPropertyValue(name: "targetItemId", value: .string(targetItemId))
try startPluginItem.setPropertyValue(name: "container", value: .string(container))
try startPluginItem.setPropertyValue(name: "state", value: .string("idle"))
PluginHandler.start(plugin: plugin, runner: startPluginItem, sceneController: sceneController, context: context)
} catch let error {
print("Error starting plugin: ", error)
}
}
}
struct CVUAction_Sync: CVUAction {
var vars: [String: CVUValue]
func execute(sceneController: SceneController, context: CVUContext, completion: ((Any?) -> ())?) {
do {
var pendingItems = [BaseRecord]()
if let item = context.currentItem {
if (item.syncState == .skip) {
item.syncState = .create
pendingItems.append(item)
}
let edges = item.edges() + item.reverseEdgeObjects()
for edge in edges {
if (edge.syncState == .skip) {
edge.syncState = .create
pendingItems.append(edge)
}
}
let edgeItems = item.edgeItems() + item.reverseEdgeItems()
for edgeItem in edgeItems {
if (edgeItem.syncState == .skip) {
edgeItem.syncState = .create
pendingItems.append(edgeItem)
}
}
}
try AppController.shared.databaseController.writeSync { (db) in
for item in pendingItems {
try item.save(db)
}
}
try AppController.shared.syncController.sync()
if (sceneController.isInEditMode) {
sceneController.toggleEditMode()
}
} catch let error {
print("Error starting plugin: ", error)
print("Error starting sync: ", error)
}
completion?(nil)
......
......@@ -114,7 +114,10 @@ extension ItemRecord {
) throws {
try dbController.writeSync { (db) in
try setPropertyValue(name: name, value: value, db: db)
self.syncState = .update
// If created, don't change to update else will go in sync payload as updatedItems
if (self.syncState != .skip && self.syncState != .create) {
self.syncState = .update
}
try self.update(db)
if (addLog) {
try addChangeLog(name: name, value: value, db: dbController)
......@@ -130,22 +133,24 @@ extension ItemRecord {
do {
switch value {
case .string(let stringValue):
try? StringRecord.setPropertyValue(name: name, value: stringValue, item: self, db: db)
try StringRecord.setPropertyValue(name: name, value: stringValue, item: self, db: db)
case .bool(let boolValue):
try? IntegerRecord.setPropertyValue(name: name, value: boolValue == true ? 1 : 0, item: self, db: db)
try IntegerRecord.setPropertyValue(name: name, value: boolValue == true ? 1 : 0, item: self, db: db)
case .int(let intValue):
try? IntegerRecord.setPropertyValue(name: name, value: intValue, item: self, db: db)
try IntegerRecord.setPropertyValue(name: name, value: intValue, item: self, db: db)
case .double(let doubleValue):
try? RealRecord.setPropertyValue(name: name, value: doubleValue, item: self, db: db)
try RealRecord.setPropertyValue(name: name, value: doubleValue, item: self, db: db)
case .datetime(let dateTimeValue):
try? RealRecord.setPropertyValue(name: name, value: Double(DatabaseHelperFunctions.encode(dateTimeValue)), item: self, db: db)
try RealRecord.setPropertyValue(name: name, value: Double(DatabaseHelperFunctions.encode(dateTimeValue)), item: self, db: db)
case .blob(let data):
if let stringValue = String(data: data, encoding: .utf8) {
try? StringRecord.setPropertyValue(name: name, value: stringValue, item: self, db: db)
try StringRecord.setPropertyValue(name: name, value: stringValue, item: self, db: db)
}
default :
break
}
} catch let error {
print("ERROR saving Property: ", error)
}
}
......
......@@ -9,6 +9,7 @@ import Foundation
import GRDB
enum SyncState: String, Codable, Hashable, DatabaseValueConvertible {
case skip
case create
case update
case noChanges
......@@ -116,11 +117,15 @@ class ItemRecord: BaseRecord, Equatable, Hashable, Identifiable, Codable {
}
}
func edges(_ name: String, db: Database) -> [EdgeRecord]? {
try? request(for: ItemRecord.edges).filter(EdgeRecord.Columns.name == name).fetchAll(db)
func edges(_ name: String? = nil, db: Database) -> [EdgeRecord]? {
if let edgeName = name {
return try? request(for: ItemRecord.edges).filter(EdgeRecord.Columns.name == edgeName).fetchAll(db)
} else {
return try? request(for: ItemRecord.edges).fetchAll(db)
}
}
func edges(_ type: String, db dbController: DatabaseController = AppController.shared.databaseController) -> [EdgeRecord] {
func edges(_ type: String? = nil, db dbController: DatabaseController = AppController.shared.databaseController) -> [EdgeRecord] {
(try? dbController.read { db in
edges(type, db: db)
}) ?? []
......@@ -132,28 +137,52 @@ class ItemRecord: BaseRecord, Equatable, Hashable, Identifiable, Codable {
}
}
func edgeItems(_ type: String, db: Database) -> [ItemRecord]? {
func edgeItems(_ type: String? = nil, db: Database) -> [ItemRecord]? {
edges(type, db: db)?.lazy.compactMap { $0.targetItem(db: db) }
}
func edgeItems(_ type: String, db dbController: DatabaseController = AppController.shared.databaseController) -> [ItemRecord] {
func edgeItems(_ type: String? = nil, db dbController: DatabaseController = AppController.shared.databaseController) -> [ItemRecord] {
(try? dbController.read { db in
edgeItems(type, db: db)
}) ?? []
}
static var reverseEdges = hasMany(EdgeRecord.self, using: EdgeRecord.targetForeignKey)
func reverseEdgeObjects(_ name: String? = nil, db: Database) -> [EdgeRecord]? {
if let edgeName = name {
return try? request(for: ItemRecord.reverseEdges).filter(EdgeRecord.Columns.name == edgeName).fetchAll(db)
} else {
return try? request(for: ItemRecord.reverseEdges).fetchAll(db)
}
}
func reverseEdgeObjects(_ type: String? = nil, db dbController: DatabaseController = AppController.shared.databaseController) -> [EdgeRecord] {
(try? dbController.read { db in
reverseEdgeObjects(type, db: db)
}) ?? []
}
func reverseEdgeItem(_ name: String, db: DatabaseController = AppController.shared.databaseController) -> ItemRecord? {
try? db.read { db in
try? request(for: ItemRecord.reverseEdges).filter(EdgeRecord.Columns.name == name).fetchOne(db)?.owningItem(db: db)
}
}
func reverseEdgeItems(_ name: String, db: DatabaseController = AppController.shared.databaseController) -> [ItemRecord] {
(try? db.read { db in
try? request(for: ItemRecord.reverseEdges).filter(EdgeRecord.Columns.name == name).fetchAll(db).lazy.compactMap { $0.owningItem(db: db) }
}) ?? []
func reverseEdgeItems(_ name: String? = nil, db: Database) -> [ItemRecord]? {
if let name = name {
return try? request(for: ItemRecord.reverseEdges).filter(EdgeRecord.Columns.name == name).fetchAll(db).lazy.compactMap { $0.owningItem(db: db) }
} else {
return try? request(for: ItemRecord.reverseEdges).fetchAll(db).lazy.compactMap { $0.owningItem(db: db) }
}
}
func reverseEdgeItems(_ name: String? = nil, db: DatabaseController = AppController.shared.databaseController) -> [ItemRecord] {
(try? db.read { db in
reverseEdgeItems(name, db: db)
}) ?? []
}
......
//
// PluginHandler.swift
// Memri
import Foundation
class PluginHandler {
public static func start(plugin: ItemRecord, runner: ItemRecord, sceneController: SceneController, context: CVUContext) {
AppController.shared.pubsubController.startObservingItemProperty(runner, property: "state", desiredValue: nil) { (newValue, error) in
guard case let .string(state) = newValue else {
return
}
switch state {
case "userActionNeeded":
presentCVUforPlugin(plugin: plugin, runner: runner, sceneController: sceneController, context: context)
default:
break
}
}
try? AppController.shared.syncController.sync()
}
static func presentCVUforPlugin(plugin: ItemRecord, runner: ItemRecord, sceneController: SceneController, context: CVUContext) {
guard let runnerRowId = runner.rowId,
let view = plugin.edgeItem("view"),
case let .string(viewName) = view.propertyValue("definition") else {
AppController.shared.pubsubController.stopObservingItemProperty(runner, property: "state")
return
}
let item = ItemRecord(type: "Account")
item.syncState = .skip // Don't sync it yet
try? item.save()
guard let itemRowId = item.rowId,
let edge = try? EdgeRecord.createFor(source: runnerRowId, type: "account", target: itemRowId) else {
return
}
edge.syncState = .skip // Don't sync it yet
try? edge.save()
var newVars: [String: CVUValue] = [:]
newVars["viewArguments"] = .subdefinition(CVUDefinitionContent(properties: ["readOnly": .constant(.bool(false))]))
CVUAction_OpenView(viewName: viewName, renderer: "generalEditor").execute(sceneController: sceneController, context: context.replacingItem(item), completion: nil)
sceneController.isInEditMode = true
}
}
......@@ -10,9 +10,9 @@ struct ItemSubscription {
let item: ItemRecord
let property: String
var retryCount: Int = 0
let desiredValue: PropertyDatabaseValue
let desiredValue: PropertyDatabaseValue?
let request: DataRequest? = nil
let completion: ((ItemRecord, Error?) -> Void)
let completion: ((PropertyDatabaseValue?, Error?) -> Void)
}
extension ItemSubscription: Hashable {
......@@ -37,8 +37,7 @@ class PubSubController {
private var cancellables: Set<AnyCancellable> = Set()
private var subscriptions: Set<AnyCancellable> = []
public func startObservingItemProperty(_ item: ItemRecord, property: String, desiredValue: PropertyDatabaseValue, completion: @escaping ((ItemRecord, Error?) -> Void)) {
public func startObservingItemProperty(_ item: ItemRecord, property: String, desiredValue: PropertyDatabaseValue?, completion: @escaping ((PropertyDatabaseValue?, Error?) -> Void)) {
stopObservingItemProperty(item, property: property)
let subscription = ItemSubscription(item: item, property: property, desiredValue: desiredValue, completion: completion)
subscribers.insert(subscription)
......@@ -54,21 +53,52 @@ class PubSubController {
}
private func subscriptionForItem(_ item: ItemRecord, property: String) -> ItemSubscription? {
return subscribers.filter{$0.item.id == item.id && $0.property == property}.first
return subscribers.filter{$0.item.id == item.id}.first
}
private func cancelSubscription(_ subscription: ItemSubscription, error: Error?) {
subscription.completion(subscription.item, error ?? StringError(description: "Cancelled"))
subscription.completion(nil, error ?? StringError(description: "Cancelled"))
subscribers.remove(subscription)
}
private func checkSubscriptionFulfilled(_ dict: [String: AnyDecodable], subscription: ItemSubscription) {
let containsValue = containsExpectedValue(dict, itemType: subscription.item.type, property: subscription.property, expectedValue: subscription.desiredValue)
private func checkSubscriptionFulfilled(_ dicts: [[String: AnyDecodable]], subscription: ItemSubscription) {
// Dict could be nil if item is still not synced
guard let dict = dicts.first, dict.count > 0 else {
reschedule(subscription)
return
}
guard let newValue = propertyValueFromDict(subscription.property, type: subscription.item.type, dict: dict) else {
cancelSubscription(subscription, error: StringError(description: "Property not found"))
return
}
guard let databaseValue = ItemRecord.fetchWithID(subscription.item.id)?.propertyValue(subscription.property) else {
cancelSubscription(subscription, error: StringError(description: "Database value not found"))
return
}
guard !containsValue else {
subscription.completion(subscription.item, nil)
subscribers.remove(subscription)
// If we have expected value to look for, check if new value is same as that of expected value
if let expectedValue = subscription.desiredValue {
let containsValue = expectedValue == newValue
guard !containsValue else {
subscription.completion(newValue, nil)
subscribers.remove(subscription)
return
}
}
// else check if the new value is different from db, in which case we just call the completion block but DO NOT stop observing
// The part where we setup observer is responsible to cancel observation in this case
if databaseValue != newValue {
// Update item in db so that next time we will detect change
if let dbItem = ItemRecord.fetchWithID(subscription.item.id) {
try? dbItem.setPropertyValue(name: subscription.property, value: newValue)
}
subscription.completion(newValue, nil)
reschedule(subscription)
return
}
......@@ -81,8 +111,16 @@ class PubSubController {
newSubscription.retryCount = newSubscription.retryCount + 1
subscribers.remove(subscription)
subscribers.insert(newSubscription)
reschedule(newSubscription)
}
private func reschedule(_ subscription: ItemSubscription) {
guard subscribers.contains(subscription) else {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + PubSubController.pollInterval) {[weak self] in
self?.startObserver(newSubscription)
self?.startObserver(subscription)
}
}
......@@ -111,32 +149,26 @@ class PubSubController {
let itemDicts = responseObjects.compactMap{object -> [String : AnyDecodable]? in
return object.compactMapValues{AnyDecodable($0)}
}
guard let itemDict = itemDicts.first else {
self?.cancelSubscription(subscription, error: StringError(description: "Error decoding item"))
return
}
self?.checkSubscriptionFulfilled(itemDict, subscription: subscription)
self?.checkSubscriptionFulfilled(itemDicts, subscription: subscription)
} .store(in: &subscriptions)
} catch let error {
cancelSubscription(subscription, error: error)
}
}
private func containsExpectedValue(_ dict: [String: AnyDecodable], itemType: String, property: String, expectedValue: PropertyDatabaseValue) -> Bool {
private func propertyValueFromDict(_ property: String, type: String, dict: [String: AnyDecodable]) -> PropertyDatabaseValue? {
guard let decodableValue = dict[property] else {
return false
return nil
}
let schema = AppController.shared.databaseController.schema
guard !ItemRecord.intrinsicProperties.contains(property),
let expectedType = schema.expectedType(forItemType: itemType, propertyName: property) else {
return false
let expectedType = schema.expectedType(forItemType: type, propertyName: property) else {
return nil
}
let databaseValue = try? PropertyDatabaseValue(value: decodableValue.value, propertyType: expectedType)
return databaseValue == expectedValue
return try? PropertyDatabaseValue(value: decodableValue.value, propertyType: expectedType)
}
}
......@@ -347,10 +347,15 @@ class SyncController {
let networkCall = try request.execute(connectionConfig: connectionConfig)
networkCall.sink { (result) in
if result.error != nil, let resultData = result.data {