Commit 4b6e071b authored by Amirjanyan's avatar Amirjanyan
Browse files

Convert from swift to js

parent 60b3b98c
Showing with 848 additions and 0 deletions
+848 -0
//
// cache.swift
// memri
//
// Created by Ruben Daniels on 3/12/20.
// Copyright © 2020 memri. All rights reserved.
//
var config = Realm.Configuration(
// Set the new schema version. This must be greater than the previously used
// version (if you've never set a schema version before, the version is 0).
51,
// Set the block which will be called automatically when opening a Realm with
// a schema version lower than the one set above
function (oldSchemaVersion) {
// We haven’t migrated anything yet, so oldSchemaVersion == 0
if (oldSchemaVersion < 2) {
// Nothing to do!
// Realm will automatically detect new properties and removed properties
// And will update the schema on disk automatically
}
}
)
/// Computes the Realm database path at /home/<user>/realm.memri/memri.realm and creates the directory (realm.memri) if it does not exist.
/// - Returns: the computed database file path
var getRealmPath = function() {
let homeDir = ProcessInfo.processInfo.environment["SIMULATOR_HOST_HOME"]
if (homeDir) {
let realmDir = homeDir + "/realm.memri"
console.log(`REALM DIR: ${realmDir}`)
try {
FileManager.default.createDirectory(
realmDir, true, null)
} catch(error) {
console.log(error)
}
return realmDir + "/memri.realm"
} else {
throw "Could not get realm path"
}
}
class CacheTODO {//TODO
/// PodAPI object
podAPI
/// Object that schedules with the POD
sync
/// Realm Database object
realm
rlmTokens = []
cancellables = []
queryIndex = {}
// TODO: document
scheduleUIUpdate
/// Starts the local realm database, which is created if it does not exist, sets the api and initializes the sync from them.
/// - Parameter api: api object
constructor(api) {
// Tell Realm to use this new configuration object for the default Realm
/*#if targetEnvironment(simulator)//TODO
do {
config.fileURL = URL(fileURLWithPath: try getRealmPath())
} catch {
// TODO: Error handling
print("\(error)")
}
#endif*/
Realm.Configuration.defaultConfiguration = config
console.log(`Starting realm at ${String(Realm.Configuration.defaultConfiguration.fileURL)}`)//TODO
// TODO: Error handling
this.realm = new Realm()
this.podAPI = api
// Create scheduler objects
this.sync = new Sync(this.podAPI, this.realm)
this.sync.cache = this
}
/// gets default item from database, and adds them to realm
install() {
// Load default database from disk
try {
let jsonData = jsonDataFromFile("default_database")
let items = MemriJSONDecoder.decode(DataItemFamily.constructor, jsonData)
realmWriteIfAvailable(this.realm, function () {
for (var item of items) {
this.realm.add(item, .modified)
}
})
} catch (error) {
console.log(`Failed to Install: ${error}`)
}
}
// TODO: Refactor: don't use async syntax when nothing is async
query(datasource) {
var error
var items
/*query(datasource) {//TODO
error = $0
items = $1
}*/
if (error) { throw error }
return items ?? []
}
/// This function does two things 1) executes a query on the local realm database with given querOptions, and executes callback on the result.
/// 2) calls the syncer with the same datasource to execute the query on the pod.
/// - Parameters:
/// - datasource: datasource for the query, containing datatype(s), filters, sortInstructions etc.
/// - callback: action exectued on the result
query1(datasource, callback) {
// Do nothing when the query is empty. Should not happen.
let q = datasource.query ?? ""
// Log to a maker user
debugHistory.info(`Executing query ${q}`)
if (q == "") {
callback("Empty Query", null)
} else {
// Schedule the query to sync from the pod
this.sync.syncQuery(datasource)
// Parse query
let [typeName, filter] = this.parseQuery(q)
let type = new DataItemFamily(typeName)
if (typeName == "*") {
var returnValue = []
for (var dtype of DataItemFamily.allCases) {
// NOTE: Allowed forced cast
let objects = this.realm.objects(dtype.getType())
.filter("deleted = false " + (filter ?? ""))//TODO
for (var item of objects) { returnValue.push(item) }
}
callback(null, returnValue)
}
// Fetch the type of the data item
else if (type) {
// Get primary key of data item
// let primKey = type.getPrimaryKey()
// Query based on a simple format:
// Query format: <type><space><filter-text>
let queryType = DataItemFamily.getType(type)
// let t = queryType() as! Object.Type
var result = this.realm.objects(queryType())
.filter("deleted = false " + (filter ?? ""))//TODO
let sortProperty = datasource.sortProperty
if (sortProperty && sortProperty != "") {
result.sort(//TODO
sortProperty,
datasource.sortAscending.value ?? true
)
}
// Construct return array
var returnValue = []
for (var item of result) {
if (item instanceof DataItem) {
returnValue.push(item)
}
}
// Done
callback(null, returnValue)
} else {
// Done
callback(`Unknown type send by server: ${q}`, null)
}
}
}
/// Parses the query string, which whould be of format \<type\>\<space\>\<filter-text\>
/// - Parameter query: query string
/// - Returns: (type to query, filter to apply)
parseQuery(query) {
if (query.indexOf(" ") >= 0) {
let splits = query.split(" ")
let type = String(splits[0])
return [type, String(splits.shift().join(" "))]
} else {
return [query, null]
}
}
getResultSet(datasource) {
// Create a unique key from query options
let key = datasource.uniqueString
// Look for a resultset based on the key
let resultSet = this.queryIndex[key]
if (resultSet) {
// Return found resultset
return resultSet
} else {
// Create new result set
let resultSet = new ResultSet(this)
// Store resultset in the lookup table
this.queryIndex[key] = resultSet
// Make sure the new resultset has the right query properties
resultSet.datasource.query = datasource.query
resultSet.datasource.sortProperty = datasource.sortProperty
resultSet.datasource.sortAscending.value = datasource.sortAscending.value
// Make sure the UI updates when the resultset updates
this.cancellables.push(resultSet.objectWillChange.sink(function () {
// TODO: Error handling
this.scheduleUIUpdate(function (context) {//TODO
this.context.cascadingView.resultSet.datasource == resultSet.datasource
})
}.bind(this)))
return resultSet
}
}
/// Adding an item to cache consist of 3 phases. 1) When the passed item already exists, it is merged with the existing item in the cache.
/// If it does not exist, this method passes a new "create" action to the SyncState, which will generate a uid for this item. 2) the merged
/// objects ia added to realm 3) We create bindings from the item with the syncstate which will trigger the syncstate to update when
/// the the item changes
/// - Parameter item:DataItem to be added
/// - Throws: Sync conflict exception
/// - Returns: cached dataItem
addToCache(item) {
try {
let newerItem = this.mergeWithCache(item)
if (newerItem) {
return newerItem
}
// Add item to realm
realm.write { realm.add(item, .modified) }//TODO
if (item.syncState?.actionNeeded == "create") {
this.sync.execute(item)
}
} catch {
console.log(`Could not add to cache: ${error}`)
}
this.bindChangeListeners(item)
return item
}
mergeWithCache(item) {
// Check if this is a new item or an existing one
let syncState = item.syncState
if (syncState) {
if (item.uid == 0) {
// Schedule to be created on the pod
realm.write {//TODO
syncState.actionNeeded = "create"
realm.add(AuditItem("create", [item]))
}
} else {
// Fetch item from the cache to double check
let cachedItem = getDataItem(item.genericType, item.memriID)//TODO
if (cachedItem) {
// Do nothing when the version is not higher then what we already have
if (!syncState.isPartiallyLoaded &&
item.version <= cachedItem.version) {
return cachedItem
}
// Check if there are local changes
if (syncState.actionNeeded != "") {
// Try to merge without overwriting local changes
if (!item.safeMerge(cachedItem)) {//TODO
// Merging failed
throw `Exception: Sync conflict with item.memriID ${cachedItem.memriID}`
}
}
// If the item is partially loaded, then lets not overwrite the database
if (syncState.isPartiallyLoaded) {
// Merge in the properties from cachedItem that are not already set
item.merge(cachedItem, true)//TODO
}
}
}
return null
} else {
console.log(`Error: no syncstate available during merge`)
return null
}
}
// TODO: does this work for subobjects?
bindChangeListeners(item) {
let syncState = item.syncState
if (syncState) {
// Update the sync state when the item changes
this.rlmTokens.push(item.observes(function (objectChange) {//TODO
let propChanges = objectChange//TODO
if (propChanges) {
if (syncState.actionNeeded == "") {
function doAction() {
// Mark item for updating
syncState.actionNeeded = "update"
syncState.changedInThisSession = true
// Record which field was updated
for (var prop of propChanges) {
if (!syncState.updatedFields.includes(prop.name)) {
syncState.updatedFields.push(prop.name)
}
}
}
realmWriteIfAvailable(this.realm) { doAction() }//TODO
}
this.scheduleUIUpdate(null)
}
}))
// Trigger sync.schedule() when the SyncState changes
// rlmTokens.append(syncState.observe { objectChange in
// if case .change = objectChange {
// if syncState.actionNeeded != "" {
// self.sync.schedule()
// }
// }
// })
} else {
console.log("Error, no syncState available for item")
}
}
/// sets delete to true in the syncstate, for an array of items
/// - Parameter item: item to be deleted
/// - Remark: All methods and properties must throw when deleted = true;
delete(item) {
if (!item.deleted) {
realmWriteIfAvailable(realm) {//TODO
item.deleted = true
item.syncState?.actionNeeded = "delete"
realm.add(AuditItem("delete", [item]))
}
}
}
/// sets delete to true in the syncstate, for an array of items
/// - Parameter items: items to be deleted
delete1(items) {
realmWriteIfAvailable(realm) {//TODO
for (var item in items) {
if (!item.deleted) {
item.deleted = true
item.setSyncStateActionNeeded("delete")
realm.add(AuditItem("delete", [item]))
}
}
}
}
/// - Parameter item: item to be duplicated
/// - Remark:Does not copy the id property
/// - Returns: copied item
duplicate(item) {
let cls = item.getType()
if (cls) {
let copy = item.getType()?.init()
if (copy) {
let primaryKey = cls.primaryKey()
for (var prop of item.objectSchema.properties) {
// TODO: allow generation of uid based on number replaces {uid}
// if (item[prop.name] as! String).includes("{uid}")
if (prop.name != primaryKey) {
copy[prop.name] = item[prop.name]
}
}
return copy
}
}
throw `Exception: Could not copy ${item.genericType}`
}
}
/// DataItem is the baseclass for all of the data clases, all functions
enum CodingKeys {
uid, memriID, deleted, starred, dateCreated, dateModified, dateAccessed, changelog,
labels, syncState
}
enum DataItemError {
cannotMergeItemWithDifferentId
}
class DataItem extends Object, Codable, Identifiable, ObservableObject {//TODO
/// name of the DataItem implementation class (E.g. "note" or "person")
genericType () { return "unknown" }
/// Title computed by implementations of the DataItem class
computedTitle() {
return `${genericType} [${memriID}]`
}
test = DataItem.generateUUID()//TODO
/// Boolean whether the DataItem has been deleted
/// uid of the DataItem set by the pod
uid = 0
/// memriID of the DataItem
memriID = DataItem.generateUUID()//TODO
/// Boolean whether the DataItem has been deleted
deleted = false
/// The last version loaded from the server
version = 0
/// Boolean whether the DataItem has been starred
starred = false
/// Creation date of the DataItem
dateCreated = new Date()//TODO
/// Last modification date of the DataItem
dateModified = new Date()//TODO
/// Last access date of the DataItem
dateAccessed = null
/// Array AuditItems describing the log history of the DataItem
changelog = []//TODO
/// Labels assigned to / associated with this DataItem
labels = []//TODO
/// Object descirbing syncing information about this object like loading state, versioning, etc.
syncState = new SyncState()//TODO
functions = {}
/// Primary key used in the realm database of this DataItem
get primaryKey() {
return "memriID"
}
cast() {
return this
}
CodingKeys = CodingKeys//TODO
DataItemError = DataItemError//TODO
constructor(decoder) {
super()
this.functions["describeChangelog"] = function() {
let dateCreated = Views.formatDate(this.dateCreated)
let views = this.changelog.filter ( function (item) {item.action == "read"} ).length
let edits = this.changelog.filter ( function (item) {item.action == "update"} ).length
let timeSinceCreated = Views.formatDateSinceCreated(this.dateCreated)
return `You created this ${this.genericType} ${dateCreated} and viewed it ${views} times and edited it ${edits} times over the past ${timeSinceCreated}`
}.bind(this)//TODO
this.functions["computedTitle"] = function() {
return this.computedTitle()
}.bind(this)//TODO
this.superDecode(decoder)
}
/// Deserializes DataItem from json decoder
/// - Parameter decoder: Decoder object
/// - Throws: Decoding error
/* public required convenience init(from decoder: Decoder) throws {//TODO
this.init()
try superDecode(from: decoder)
}*/
/// @private
superDecode(decoder) {//TODO
this.uid = decoder.decodeIfPresent("uid") || this.uid
this.memriID = decoder.decodeIfPresent("memriID") || this.memriID
this.starred = decoder.decodeIfPresent("starred") || this.starred
this.deleted = decoder.decodeIfPresent("deleted") || this.deleted
this.version = decoder.decodeIfPresent("version") || this.version
this.syncState = decoder.decodeIfPresent("syncState") || this.syncState
this.dateCreated = decoder.decodeIfPresent("dateCreated") || this.dateCreated
this.dateModified = decoder.decodeIfPresent("dateModified") || this.dateModified
this.dateAccessed = decoder.decodeIfPresent("dateAccessed") || this.dateAccessed
this.decodeIntoList(decoder, "changelog", this.changelog)
this.decodeIntoList(decoder, "labels", this.labels)
}
/// Get string, or string representation (e.g. "true) from property name
/// - Parameter name: property name
/// - Returns: string representation
getString(name) {
if (this.objectSchema[name] == null) {
/*#if DEBUG
print("Warning: getting property that this dataitem doesnt have: \(name) for \(genericType):\(memriID)")
#endif*/
return ""
} else {
let val = this[name]
var typeofVal = typeof val;
if (typeofVal === "string") {
return val
} else if (typeofVal === "boolean") {
return String(val)
} else if (typeofVal === "number") {
return String(val)
// } else if let val = val as? Double {
// return String(val)
} else if (val instanceof Date) {//TODO ?
let formatter = new DateFormatter()
formatter.dateFormat = Settings.get("user/formatting/date") // "HH:mm dd/MM/yyyy"
return formatter.string(val)
} else {
return ""
}
}
}
/// Get the type of DataItem
/// - Returns: type of the DataItem
getType() {
let type = new DataItemFamily(this.genericType)
if (type) {
let T = DataItemFamily.getType(type)//TODO
// NOTE: allowed forced downcast
return (T())
} else {
console.log(`Cannot find type ${genericType} in DataItemFamily`)
return null
}
}
/// Determines whether item has property
/// - Parameter propName: name of the property
/// - Returns: boolean indicating whether DataItem has the property
hasProperty(propName) {
if (propName == "self") {
return true
}
for (var prop of this.objectSchema.properties) {
if (prop.name == propName) { return true }
let haystack = this[prop.name]
if (typeof haystack === "string") {
if (haystack.toLowerCase().indexOf(propName.toLowerCase()) > -1) {
return true
}
}
}
return false
}
/// Get property value
/// - Parameters:
/// - name: property name
get(name, type?) {
if (name == "self") {
return this
}
return this[name]
}
/// Set property to value, which will be persisted in the local database
/// - Parameters:
/// - name: property name
/// - value: value
set(name, value) {
realmWriteIfAvailable(realm) {//TODO
this[name] = value
}
}
addEdge(propertyName, item) {
let subjectID = this.get("memriID")
if (!subjectID) return
let objectID = item.get("memriID")
if (!objectID) return
let edges = this.get(propertyName) ?? []
if (!edges.map(function(item) { item.objectMemriID }).includes(objectID)) {
let newEdge = new Edge(subjectID, objectID, "Label", "Note")
let newEdges = edges.concat([newEdge])
this.set("appliesTo", newEdges)
} else {
throw "Could note create Edge, already exists"
}
// // Check that the property exists to avoid hard crash
// guard let schema = this.objectSchema[propertyName] else {
// throw "Exception: Invalid property access of \(item) for \(self)"
// }
// guard let objectID: String = item.get("memriID") else {
// throw "no memriID"
// }
//
// if schema.isArray {
// // Get list and append
// var list = dataItemListToArray(self[propertyName] as Any)
//
// if !list.map{$0.memriID}.contains(objectID){
// list.append(item)
// print(list)
// this.set(propertyName, list as Any)
// }
// else {
// print("Could not set edge, already exists")
// }
// }
// else {
// this.set(propertyName, item)
// }
}
/// Toggle boolean property
/// - Parameter name: property name
toggle(name) {
let val = this[name]
if (typeof val === "boolean") {
this.set(name, val)
} else {
console.log(`tried to toggle property ${name}, but ${name} is not a boolean`)
}
}
/// Compares value of this DataItems property with the corresponding property of the passed items property
/// - Parameters:
/// - propName: name of the compared property
/// - item: item to compare against
/// - Returns: boolean indicating whether the property values are the same
isEqualProperty(propName, item) {
let prop = this.objectSchema[propName]
if (prop) {
// List
if (prop.objectClassName != null) {
return false // TODO: implement a list compare and a way to add to updatedFields
} else {
let value1 = this[propName]
let value2 = item[propName]
let item1 = value1
if (typeof item1 === "string" && typeof value2 === "string") {
return item1 == value2
}
if (typeof item1 === "number" && typeof value2 === "number") {
return item1 == value2
}
// if let item1 = value1 as? Double, let value2 = value2 as? Double {
// return item1 == value2
// }
if (typeof item1 === "object" && typeof value2 === "object") {
return item1 == value2
} else {
// TODO: Error handling
console.log(`Trying to compare property ${propName} of item ${item} and ${this}
but types do not mach`)
}
}
return true
} else {
// TODO: Error handling
console.log(`Tried to compare property ${propName}, but ${this} does not have that property`)
return false
}
}
/// Safely merges the passed item with the current DataItem. When there are merge conflicts, meaning that some other process
/// requested changes for the same properties with different values, merging is not performed.
/// - Parameter item: item to be merged with the current DataItem
/// - Returns: boolean indicating the succes of the merge
safeMerge(item) {
let syncState = this.syncState
if (syncState) {
// Ignore when marked for deletion
if (syncState.actionNeeded == "delete") { return true }
// Do not update when the version is not higher then what we already have
if (item.version <= this.version) { return true }
// Make sure to not overwrite properties that have been changed
let updatedFields = syncState.updatedFields
// Compare all updated properties and make sure they are the same
for (var fieldName of updatedFields) {
if (!this.isEqualProperty(fieldName, item)) { return false }
}
// Merge with item
this.merge(item)
return true
} else {
// TODO: Error handling
console.log("trying to merge, but syncState is null")
return false
}
}
/// merges the the passed DataItem in the current item
/// - Parameters:
/// - item: passed DataItem
/// - mergeDefaults: boolean describing how to merge. If mergeDefault == true: Overwrite only the property values have
/// not already been set (null). else: Overwrite all property values with the values from the passed item, with the exception
/// that values cannot be set from a non-null value to null.
merge(item, mergeDefaults = false) {
// Store these changes in realm
let realm = this.realm
if (realm) {
try {
realm.write { this.doMerge(item, mergeDefaults) }//TODO
} catch(error) {
console.log(`Could not write merge of ${item} and ${this} to realm`)
}
} else {
this.doMerge(item, mergeDefaults)
}
}
doMerge(item, mergeDefaults = false) {
let properties = this.objectSchema.properties
for (var prop of properties) {
// Exclude SyncState
if (prop.name == "SyncState") {
continue
}
// Perhaps not needed:
// - TODO needs to detect lists which will always be set
// - TODO needs to detect optionals which will always be set
// Overwrite only the property values that are not already set
if (mergeDefaults) {
if (this[prop.name] == null) {
this[prop.name] = item[prop.name]
}
}
// Overwrite all property values with the values from the passed item, with the
// exception, that values cannot be set ot null
else {
if (item[prop.name] != null) {
this[prop.name] = item[prop.name]
}
}
}
}
/// update the dateAccessed property to the current date
access() {
realmWriteIfAvailable(realm) {//TODO
this.dateAccessed = Date()
}
}
/// compare two dataItems
/// - Parameters:
/// - lhs: DataItem 1
/// - rhs: DataItem 2
/// - Returns: boolean indicating equality
/*public static func == (lhs: DataItem, rhs: DataItem) -> Bool {//TODO
lhs.memriID == rhs.memriID
}*/
/// Generate a new UUID, which are used by swift to identify objects
/// - Returns: UUID string with "0xNEW" prepended
static generateUUID() {//TODO
return `Memri${UUID().uuidString}`
}
/// Reads DataItems from file
/// - Parameters:
/// - file: filename (without extension)
/// - ext: extension
/// - Throws: Decoding error
/// - Returns: Array of deserialized DataItems
fromJSONFile(file, ext = "json") {
let jsonData = jsonDataFromFile(file, ext)//TODO
let items = MemriJSONDecoder.decode(DataItemFamily.constructor, jsonData)//TODO
return items
}
/// Sets syncState .actionNeeded property
/// - Parameters:
/// - action: action name
setSyncStateActionNeeded(action) {
let syncState = this.syncState
if (syncState) {
syncState.actionNeeded = action
} else {
console.log(`No syncState available for item ${self}`)
}
}
/// Read DataItem from string
/// - Parameter json: string to parse
/// - Throws: Decoding error
/// - Returns: Array of deserialized DataItems
fromJSONString(json) {
let items = MemriJSONDecoder
.decode(DataItemFamily.constructor, new Data(json.utf8))//TODO
return items
}
}
class Edge extends Object {
objectMemriID = DataItem.generateUUID()//TODO
subjectMemriID = DataItem.generateUUID()//TODO
objectType = "unknown"
subjectType = "unknown"
// required init() {}//TODO
constructor(subjectMemriID, objectMemriID, subjectType = "unknown", objectType = "unknown") {
super()
subjectMemriID = subjectMemriID || DataItem.generateUUID()
objectMemriID = objectMemriID || DataItem.generateUUID()
this.objectMemriID = objectMemriID
this.subjectMemriID = subjectMemriID
this.objectType = objectType
this.subjectType = subjectType
}
// maybe we dont need this
// @objc dynamic var objectType:String = DataItem.generateUUID()
// @objc dynamic var subectType:String = DataItem.generateUUID()
/// Deserializes DataItem from json decoder
/// - Parameter decoder: Decoder object
/// - Throws: Decoding error
// required public convenience init(from decoder: Decoder) throws{
// this.init()
// objectUid = try decoder.decodeIfPresent("objectUid") ?? objectUid
// subjectUid = try decoder.decodeIfPresent("subjectUid") ?? subjectUid
// }
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment