2020-01-11 19:42:28 +00:00
//
// Q u e r y V i e w C o n t r o l l e r . s w i f t
// M o n g o V i e w
//
// C r e a t e d b y S h a d o w f a c t s o n 1 / 9 / 2 0 .
// C o p y r i g h t © 2 0 2 0 S h a d o w f a c t s . A l l r i g h t s r e s e r v e d .
//
import Cocoa
import MongoSwift
2020-08-12 23:09:43 +00:00
import NIO
2020-01-11 19:42:28 +00:00
class QueryViewController : NSViewController {
@IBOutlet weak var verticalSplitView : NSSplitView !
2020-04-04 18:35:13 +00:00
@IBOutlet var filterTextView : JavaScriptEditorView !
2020-01-11 19:42:28 +00:00
@IBOutlet weak var outlineView : NSOutlineView !
@IBOutlet weak var documentCountLabel : NSTextField !
let mongoController : MongoController
let collection : DatabaseCollection
2020-04-04 18:35:13 +00:00
var defaultFilter : String {
" {} "
2020-01-11 19:42:28 +00:00
}
2020-04-04 18:35:13 +00:00
var hasFilterChanged : Bool {
return filterTextView . string != defaultFilter
2020-01-11 19:42:28 +00:00
}
2020-02-10 02:13:22 +00:00
var mostRecentQuery : String ? = nil
2020-01-11 19:42:28 +00:00
var rootNodes : [ Node ] = [ ]
init ( mongoController : MongoController , collection : DatabaseCollection ) {
self . mongoController = mongoController
self . collection = collection
super . init ( nibName : " QueryViewController " , bundle : . main )
}
required init ? ( coder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
override func viewDidLoad ( ) {
super . viewDidLoad ( )
2020-02-10 02:13:22 +00:00
refresh ( )
2020-01-11 19:42:28 +00:00
verticalSplitView . delegate = self
verticalSplitView . setHoldingPriority ( . defaultHigh , forSubviewAt : 0 )
2020-04-04 18:35:13 +00:00
filterTextView . isAutomaticQuoteSubstitutionEnabled = false
filterTextView . string = defaultFilter
2020-04-04 17:25:51 +00:00
2020-01-11 19:42:28 +00:00
outlineView . dataSource = self
outlineView . delegate = self
outlineView . target = self
outlineView . doubleAction = #selector ( outlineCellDoubleClicked )
}
override func viewWillAppear ( ) {
super . viewWillAppear ( )
verticalSplitView . setPosition ( 80 , ofDividerAt : 0 )
}
2020-04-04 17:25:51 +00:00
2020-02-10 02:13:22 +00:00
func refresh ( reload : Bool = true ) {
2020-04-04 18:35:13 +00:00
let filterText = filterTextView . string . trimmingCharacters ( in : . whitespacesAndNewlines )
2020-08-12 23:09:43 +00:00
let filter : BSONDocument
2020-04-04 18:35:13 +00:00
if ! filterText . isEmpty ,
2020-04-06 23:31:26 +00:00
let doc = ExtendedJSON . toDocument ( filterText ) {
2020-04-04 18:35:13 +00:00
filter = doc
2020-02-10 02:13:22 +00:00
} else {
2020-04-04 18:35:13 +00:00
filter = [ : ]
2020-02-10 02:13:22 +00:00
}
2020-04-04 18:35:13 +00:00
2020-08-12 23:09:43 +00:00
mongoController . statusManager . set ( " Querying \( collection ) ... " , for : . query , override : true )
2020-04-04 18:35:13 +00:00
2020-08-12 23:09:43 +00:00
let collection = mongoController . collection ( self . collection )
collection . find ( filter ) . flatMap { ( cursor : MongoCursor ) -> EventLoopFuture < [ BSONDocument ] > in
return cursor . toArray ( )
} . whenSuccess { ( documents ) in
DispatchQueue . main . async {
self . rootNodes = documents . map { Node ( document : $0 ) }
2020-08-12 23:42:40 +00:00
self . title = self . collection . description
2020-08-12 23:09:43 +00:00
self . documentCountLabel . stringValue = " \( documents . count ) document \( documents . count = = 1 ? " " : " s " ) "
if reload {
self . outlineView . reloadData ( )
}
2020-08-12 23:42:40 +00:00
self . mongoController . statusManager . set ( " Queried \( self . collection ) " , for : . query , override : true )
2020-08-12 23:09:43 +00:00
}
2020-02-10 02:13:22 +00:00
}
}
2020-02-10 02:13:46 +00:00
func deleteRootNode ( _ node : Node ) {
guard case let . document ( doc ) = node . value else { return }
let alert = NSAlert ( )
alert . alertStyle = . warning
alert . messageText = " Confirm deletion "
alert . informativeText = " Are you sure you want to delete the document "
2020-08-12 23:09:43 +00:00
let id : BSONObjectID ?
if case let . objectID ( docId ) = doc [ " _id " ] {
2020-02-10 02:13:46 +00:00
id = docId
alert . informativeText += " with id \( docId ) "
} else {
id = nil
}
2020-01-12 15:39:24 +00:00
2020-02-10 02:13:46 +00:00
alert . addButton ( withTitle : " Delete " )
alert . addButton ( withTitle : " Cancel " )
alert . beginSheetModal ( for : view . window ! ) { ( response ) in
guard response = = . alertFirstButtonReturn else { return }
self . mongoController . collection ( self . collection ) . deleteOne ( doc ) . whenComplete { ( result ) in
DispatchQueue . main . async {
switch result {
case let . success ( result ) :
guard let result = result , result . deletedCount = = 1 else {
let alert = NSAlert ( )
alert . alertStyle = . critical
alert . messageText = " Error deleting document "
if let id = id {
alert . informativeText = " The document with id \( id ) could not be deleted. "
} else {
alert . informativeText = " The document could not be deleted. "
}
alert . beginSheetModal ( for : self . view . window ! , completionHandler : nil )
return
}
self . refresh ( )
2020-07-07 17:23:27 +00:00
self . mongoController . statusManager . set ( " Deleted document " , for : . document )
2020-02-10 02:13:46 +00:00
case let . failure ( error ) :
let alert = NSAlert ( error : error )
alert . beginSheetModal ( for : self . view . window ! , completionHandler : nil )
}
}
}
2020-01-12 15:39:24 +00:00
}
2020-01-11 19:42:28 +00:00
}
2020-08-12 22:49:56 +00:00
private func nodeForCopying ( ) -> Node ? {
if outlineView . clickedRow >= 0 {
return outlineView . item ( atRow : outlineView . clickedRow ) as ? Node
} else {
return outlineView . item ( atRow : outlineView . selectedRow ) as ? Node
}
}
2020-08-13 02:45:37 +00:00
private func openEditWindow ( _ document : BSONDocument ) {
let wc = EditDocumentWindowController ( mongoController : mongoController , collection : collection , document : document )
wc . documentEdited = {
self . refresh ( )
self . mongoController . statusManager . set ( " Updated document " , for : . document )
}
wc . showWindow ( nil )
}
2020-01-11 19:42:28 +00:00
@objc func outlineCellDoubleClicked ( ) {
2020-08-13 02:45:37 +00:00
if let node = outlineView . item ( atRow : outlineView . clickedRow ) as ? Node {
if node . hasChildren {
if outlineView . isItemExpanded ( node ) {
outlineView . collapseItem ( node )
} else {
outlineView . expandItem ( node )
}
} else if node . isValueInlineEditable {
outlineView . editColumn ( 1 , row : outlineView . clickedRow , with : nil , select : false )
2020-01-11 19:42:28 +00:00
}
}
}
2020-02-10 02:13:46 +00:00
@IBAction func deleteNode ( _ sender : Any ) {
guard let node = outlineView . item ( atRow : outlineView . clickedRow ) as ? Node else {
return
}
if node . parent = = nil {
deleteRootNode ( node )
}
}
2020-01-11 19:42:28 +00:00
2020-04-06 23:31:26 +00:00
@IBAction func editDocument ( _ sender : Any ) {
guard let node = outlineView . item ( atRow : outlineView . clickedRow ) as ? Node ,
node . parent = = nil ,
case let . document ( document ) = node . value else {
return
}
2020-08-13 02:45:37 +00:00
openEditWindow ( document )
2020-04-06 23:31:26 +00:00
}
2020-08-12 22:49:56 +00:00
@IBAction func copy ( _ sender : Any ) {
guard let node = nodeForCopying ( ) else { return }
NSPasteboard . general . clearContents ( )
NSPasteboard . general . setString ( node . valueString , forType : . string )
// t o d o : s u p p o r t c o p y i n g m o r e s p e c i f i c t y p e s ?
}
2020-08-12 23:37:11 +00:00
@IBAction func copyAsJSON ( _ sender : Any ) {
guard let node = nodeForCopying ( ) else { return }
let doc : BSONDocument = [ " value " : node . value ]
let ext = doc . toExtendedJSONString ( )
// t o E x t e n d e d J S O N r e t u r n s ` { " v a l u e " : < w h a t e v e r > } ` , d r o p t h e o b j e c t w r a p p e r
let str = String ( ext . dropFirst ( 12 ) . dropLast ( 2 ) )
NSPasteboard . general . clearContents ( )
NSPasteboard . general . setString ( str , forType : . string )
}
2020-08-13 02:45:37 +00:00
@objc func editedValue ( _ textField : NSTextField ) {
guard let node = outlineView . item ( atRow : outlineView . selectedRow ) as ? Node ,
case let . document ( rootDoc ) = node . root . value else {
return
}
let proposedValue = textField . stringValue
if let newValue = node . coerceBSONValue ( proposedValue ) {
let updateDoc : BSONDocument = [
" $set " : [
node . buildUpdateKey ( ) : newValue
]
]
mongoController . collection ( collection ) . updateOne ( filter : rootDoc , update : updateDoc ) . whenComplete { ( result ) in
switch result {
case . success ( nil ) :
fatalError ( )
case . success ( _ ) :
self . mongoController . statusManager . set ( " Updated document " , for : . document )
case let . failure ( error ) :
DispatchQueue . main . async {
let alert = NSAlert ( error : error )
alert . beginSheetModal ( for : self . view . window ! , completionHandler : nil )
}
}
}
} else {
textField . stringValue = node . valueString
let alert = NSAlert ( )
alert . alertStyle = . critical
alert . messageText = " Invalid value format "
alert . informativeText = " The value ' \( proposedValue ) ' is not valid for fields of type \( node . value . type ) . \n If you want to change the value type, edit the JSON document. "
alert . addButton ( withTitle : " OK " )
alert . addButton ( withTitle : " Edit Document " )
alert . beginSheetModal ( for : self . view . window ! ) { ( res ) in
alert . window . close ( )
if res = = . alertSecondButtonReturn {
self . openEditWindow ( rootDoc )
}
}
}
}
2020-01-11 19:42:28 +00:00
}
2020-04-03 02:59:05 +00:00
2020-02-10 02:13:22 +00:00
extension QueryViewController : NSMenuItemValidation {
func validateMenuItem ( _ menuItem : NSMenuItem ) -> Bool {
2020-04-06 23:31:26 +00:00
if menuItem . action = = #selector ( deleteNode ( _ : ) ) || menuItem . action = = #selector ( editDocument ( _ : ) ) {
2020-04-04 17:28:25 +00:00
if outlineView . clickedRow != - 1 , let node = outlineView . item ( atRow : outlineView . clickedRow ) as ? Node , node . parent = = nil {
return true
} else {
return false
}
2020-08-12 23:37:11 +00:00
} else if menuItem . action = = #selector ( copy ( _ : ) ) || menuItem . action = = #selector ( copyAsJSON ( _ : ) ) {
2020-08-12 22:49:56 +00:00
let node = nodeForCopying ( )
return node != nil && node ! . isValueCopyable
2020-02-10 02:13:22 +00:00
}
return true
}
}
2020-01-11 19:42:28 +00:00
extension QueryViewController : NSSplitViewDelegate {
func splitView ( _ splitView : NSSplitView , constrainSplitPosition proposedPosition : CGFloat , ofSubviewAt dividerIndex : Int ) -> CGFloat {
return max ( 80 , min ( splitView . bounds . height / 2 , proposedPosition ) )
}
}
extension QueryViewController : NSOutlineViewDataSource {
func outlineView ( _ outlineView : NSOutlineView , numberOfChildrenOfItem item : Any ? ) -> Int {
if item = = nil {
return rootNodes . count
} else if let node = item as ? Node {
return node . numberOfChildren
} else {
return 0
}
}
func outlineView ( _ outlineView : NSOutlineView , isItemExpandable item : Any ) -> Bool {
if let node = item as ? Node {
return node . hasChildren
} else {
return false
}
}
func outlineView ( _ outlineView : NSOutlineView , child index : Int , ofItem item : Any ? ) -> Any {
if item = = nil {
return rootNodes [ index ]
} else if let node = item as ? Node {
return node . children [ index ]
} else {
fatalError ( " unreachable " )
}
}
}
extension QueryViewController : NSOutlineViewDelegate {
func outlineView ( _ outlineView : NSOutlineView , viewFor tableColumn : NSTableColumn ? , item : Any ) -> NSView ? {
guard let tableColumn = tableColumn ,
let node = item as ? Node else {
fatalError ( )
}
if tableColumn . identifier = = . fieldNameColumn {
let cell = outlineView . makeView ( withIdentifier : . fieldNameCell , owner : nil ) as ! NSTableCellView
cell . textField ! . stringValue = node . keyString
cell . textField ! . isEditable = false
return cell
} else if tableColumn . identifier = = . fieldValueColumn {
let cell = outlineView . makeView ( withIdentifier : . fieldValueCell , owner : nil ) as ! NSTableCellView
cell . textField ! . stringValue = node . valueString
2020-08-13 02:45:37 +00:00
cell . textField ! . isEditable = node . isValueInlineEditable
cell . textField ! . target = self
cell . textField ! . action = #selector ( editedValue ( _ : ) )
2020-01-11 19:42:28 +00:00
return cell
2020-01-12 16:05:01 +00:00
} else if tableColumn . identifier = = . valueTypeColumn {
let cell = outlineView . makeView ( withIdentifier : . valueTypeCell , owner : nil ) as ! NSTableCellView
cell . textField ! . stringValue = node . typeString
cell . textField ! . isEditable = false
return cell
2020-01-11 19:42:28 +00:00
} else {
return nil
}
}
}
extension NSUserInterfaceItemIdentifier {
static let fieldNameColumn = NSUserInterfaceItemIdentifier ( rawValue : " FieldNameCol " )
static let fieldValueColumn = NSUserInterfaceItemIdentifier ( rawValue : " FieldValueCol " )
static let fieldNameCell = NSUserInterfaceItemIdentifier ( rawValue : " FieldNameCell " )
static let fieldValueCell = NSUserInterfaceItemIdentifier ( rawValue : " FieldValueCell " )
2020-01-12 16:05:01 +00:00
static let valueTypeColumn = NSUserInterfaceItemIdentifier ( rawValue : " ValueTypeCol " )
static let valueTypeCell = NSUserInterfaceItemIdentifier ( rawValue : " ValueTypeCell " )
2020-01-11 19:42:28 +00:00
}
2020-08-13 02:45:37 +00:00
fileprivate extension Node {
var isValueCopyable : Bool {
switch value . type {
case . document , . array , . binary , . minKey , . maxKey :
return false
default :
return true
}
}
var isValueInlineEditable : Bool {
switch value . type {
case . double , . string , . objectID , . bool , . datetime , . int32 , . int64 , . decimal128 :
return true
default :
return false
}
}
func coerceBSONValue ( _ str : String ) -> BSON ? {
guard isValueInlineEditable else { return false }
switch value . type {
case . double :
if let d = Double ( str ) {
return . double ( d )
} else {
return nil
}
case . string :
return . string ( str )
case . objectID :
if let id = try ? BSONObjectID ( str ) {
return . objectID ( id )
} else {
return nil
}
case . bool :
let lower = str . lowercased ( )
if lower = = " true " {
return . bool ( true )
} else if lower = = " false " {
return . bool ( false )
} else {
return nil
}
case . datetime :
if let date = Node . dateFormatter . date ( from : str ) {
return . datetime ( date )
} else {
return nil
}
case . int32 :
if let i = Int32 ( str ) {
return . int32 ( i )
} else {
return nil
}
case . int64 :
if let i = Int64 ( str ) {
return . int64 ( i )
} else {
return nil
}
case . decimal128 :
if let dec = try ? BSONDecimal128 ( str ) {
return . decimal128 ( dec )
} else {
return nil
}
default :
return nil
}
}
func buildUpdateKey ( ) -> String {
let parentKey : String
if let parent = parent {
if case . objectID ( _ ) = parent . key , parent . parent = = nil {
parentKey = " "
} else {
parentKey = parent . buildUpdateKey ( ) + " . "
}
} else {
parentKey = " "
}
switch key {
case let . index ( index ) :
return parentKey + index . description
case let . name ( name ) :
return parentKey + name
default :
fatalError ( )
}
}
}