diff --git a/DESCRIPTION b/DESCRIPTION index 152438c..2b44d46 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: neo4r Title: A 'Neo4J' Driver -Version: 0.1.3 +Version: 0.1.4 Authors@R: c(person(given = "Colin", family = "Fay", @@ -44,4 +44,4 @@ Imports: utils Encoding: UTF-8 LazyData: true -RoxygenNote: 6.1.0 +RoxygenNote: 6.1.1 diff --git a/NAMESPACE b/NAMESPACE index 8b61a9d..975428f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -7,6 +7,7 @@ export(extract_relationships) export(launch_con_pane) export(load_csv) export(neo4j_api) +export(neo4r_cypher) export(on_connection_opened) export(play_movies) export(read_cypher) @@ -30,7 +31,6 @@ importFrom(httr,add_headers) importFrom(httr,content) importFrom(httr,status_code) importFrom(jsonlite,base64_enc) -importFrom(jsonlite,toJSON) importFrom(magrittr,"%>%") importFrom(purrr,as_vector) importFrom(purrr,compact) @@ -39,6 +39,7 @@ importFrom(purrr,flatten_dfr) importFrom(purrr,map) importFrom(purrr,map_chr) importFrom(purrr,map_df) +importFrom(purrr,map_dfr) importFrom(purrr,map_int) importFrom(purrr,map_lgl) importFrom(purrr,modify_depth) diff --git a/R/R6.R b/R/R6.R index 6a9e10a..3a42cbd 100644 --- a/R/R6.R +++ b/R/R6.R @@ -22,7 +22,7 @@ #' @importFrom attempt attempt #' @importFrom R6 R6Class #' @importFrom httr GET status_code add_headers content -#' @importFrom purrr map as_vector +#' @importFrom purrr map map_dfr as_vector #' @importFrom tibble as_tibble tibble #' @importFrom data.table rbindlist #' @importFrom tidyr unnest @@ -46,18 +46,20 @@ neo4j_api <- R6::R6Class( relationships = data.frame(0), auth = character(0), labels = data.frame(0), + db = character(0), print = function() { cat("\n") if (self$ping() == 200) { cat("Connected at", self$url, "\n") cat("User:", self$user, "\n") + cat("Neo4j database:", self$db, "\n") cat("Neo4j version:", self$get_version(), "\n") } else { cat("No registered Connection\n") cat("(Wrong credentials or hostname)\n") } }, - initialize = function(url, user, password) { + initialize = function(url, user, password, db = "neo4j") { # browser() # Clean url in case it ends with a / if (grepl("bolt", url)) { @@ -74,6 +76,7 @@ neo4j_api <- R6::R6Class( self$user <- user private$password <- password self$auth <- base64_enc(paste0(user, ":", password)) + self$db <- db }, reset_url = function(url) { self$url <- url @@ -86,6 +89,9 @@ neo4j_api <- R6::R6Class( private$password <- password self$auth <- base64_enc(paste0(self$user, ":", private$password)) }, + reset_db = function(db) { + self$db <- db + }, # List elements access = function() { list( @@ -98,40 +104,104 @@ neo4j_api <- R6::R6Class( # if we should make if verbose instead of just returning SC ping = function() { # browser() - attempt(status_code(get_wrapper(self, "db/data/relationship/types"))) + attempt(status_code(GET(self$url))) }, # Get Neo4J version get_version = function() { - res <- get_wrapper(self, "db/data") - content(res)$neo4j_version + # get the root endpoint for the connection + res <- GET(self$url) + root <- content(res) + + # determine if root endpoint content indicates v3.x or v4.x of Neo4j + # there may be better ways to discover the version, but this works for now + if ("neo4j_version" %in% names(root)) { + version <- root$neo4j_version # this is version 4.x + } + else { + res <- get_wrapper(self, "db/data") + version <- content(res)$neo4j_version # this is version 3.x + } + version }, # Get a list of relationship registered in the db # return it as a data.frame because data.frame are cool get_relationships = function() { - res <- get_wrapper(self, "db/data/relationship/types") - tibble(labels = as.character(content(res))) + if (self$major_version < 4) { + res <- get_wrapper(self, "db/data/relationship/types") + typs <- tibble(labels = as.character(content(res))) + } else { + typs <- 'CALL db.relationshipTypes' %>% + call_neo4j(self) %>% + map(unlist) %>% + as_tibble() + } + typs }, # Get a list of labels registered in the db # Tibbles are awesome get_labels = function() { - res <- get_wrapper(self, "db/data/labels") - tibble(labels = as.character(content(res))) + if (self$major_version < 4) { + res <- get_wrapper(self, "db/data/labels") + labs <- tibble(labels = as.character(content(res))) + } else { + labs <- 'CALL db.labels' %>% + call_neo4j(self) %>% + map(unlist) %>% + as_tibble() + } + labs }, get_property_keys = function() { - res <- get_wrapper(self, "db/data/propertykeys") - tibble(labels = as.character(content(res))) + if (self$major_version < 4) { + res <- get_wrapper(self, "db/data/propertykeys") + props <- tibble(labels = as.character(content(res))) + } else { + props <- 'CALL db.propertyKeys' %>% + call_neo4j(self) %>% + map(unlist) %>% + as_tibble() + } + props }, # Get the schema of the db # There must be a better way to parse this get_index = function() { - res <- get_wrapper(self, "db/data/schema/index") - map(content(res), as_tibble) %>% map(unnest) %>% rbindlist() + if (self$major_version < 4) { + res <- get_wrapper(self, "db/data/schema/index") + idx <- map_dfr(content(res), as_tibble) %>% unnest(c(property_keys, labels)) + } else { + idx <- glue("CALL db.indexes() yield id, name, state, populationPercent, uniqueness, type, entityType, labelsOrTypes, properties, provider ", + "UNWIND labelsOrTypes as label ", + "UNWIND properties as property ", + "RETURN id, name, state, populationPercent, uniqueness, type, entityType, label, property, provider") %>% + call_neo4j(self) %>% + map(unlist) %>% + as_tibble() + } + idx }, # Get a list of constraints registered in the db # Same here get_constraints = function() { - res <- get_wrapper(self, "db/data/schema/constraint") - map(content(res), as_tibble) %>% map(unnest) %>% rbindlist() + if (self$major_version < 4) { + res <- get_wrapper(self, "db/data/schema/constraint") + cons <- map_dfr(content(res), as_tibble) %>% unnest(c(property_keys, type)) + } else { + cons <- "CALL db.constraints" %>% + call_neo4j(self) %>% + map(unlist) %>% + as_tibble() + } + cons + } + ), + active = list( + # get the major version of Neo4j as a number + # save converting this later from the get_version() string + major_version = function() { + self$get_version() %>% + substr(1,1) %>% + as.integer() } ) ) diff --git a/R/call_api.R b/R/call_api.R index 15b4a0b..78e3401 100644 --- a/R/call_api.R +++ b/R/call_api.R @@ -5,24 +5,11 @@ clean_query <- function(query) { res } -#' @importFrom jsonlite toJSON - -to_json_neo <- function(query, include_stats, meta, type) { - toJSON( - list( - statement = query, - includeStats = include_stats, - meta = meta, - resultDataContents = list(type) - ), - auto_unbox = TRUE - ) -} - #' Call Neo4J API #' #' @param query The cypher query #' @param con A NEO4JAPI connection object +#' @param params A list of parameters to pass along with the cypher query #' @param type Return the result as row or as graph #' @param output Use "json" if you want the output to be printed as JSON #' @param include_stats tShould the stats about the transaction be included? @@ -35,7 +22,7 @@ to_json_neo <- function(query, include_stats, meta, type) { #' @return the result from the Neo4J Call #' @export -call_neo4j <- function(query, con, +call_neo4j <- function(query, con, params = list(), type = c("row", "graph"), output = c("r", "json"), include_stats = FALSE, @@ -53,21 +40,44 @@ call_neo4j <- function(query, con, # Clean the query to prevent weird mess up with " and stuffs query_clean <- clean_query(query) - # Transform the query to a Neo4J JSON format - query_jsonised <- to_json_neo(query_clean, include_stats, include_meta, type) - # Unfortunately I was not able to programmatically convert everything to JSON - body <- glue('{"statements" : [ %query_jsonised% ]}', .open = "%", .close = "%") + # if parameters are empty then they need to be a vector + # in order for the json POST body to be formatted correctly + if (length(params) == 0) params <- c() + + post_body <- list( + statements = list( + list( + statement = query_clean, + parameters = params, + resultDataContents = list(type) + ) + ) + ) + + # add include_stats and meta based on user preferences + if (include_stats) body$statements[[1]]$includeStats = include_stats + if (include_meta) body$statements[[1]]$meta = include_meta + + # POST URL (different for pre-4.x vs. 4.x) + if (con$major_version < 4) { + # for Neo4j versions before 4.x + post_url <- glue("{con$url}/db/data/transaction/commit") + } else { + # for Neo4j version 4.x + post_url <- glue("{con$url}/db/{con$db}/tx/commit") + } # Calling the API res <- POST( - url = glue("{con$url}/db/data/transaction/commit?includeStats=true"), + url = post_url, add_headers(.headers = c( "Content-Type" = "application/json", "accept" = "application/json", # "X-Stream" = "true", "Authorization" = paste0("Basic ", con$auth) )), - body = body + body = post_body, + encode = "json" ) # Verify the status code is 200 @@ -87,6 +97,19 @@ call_neo4j <- function(query, con, } } +#' Run A Cypher Query (but start with the connection object) +#' +#' @param con A NEO4JAPI connection object +#' @param query The cypher query +#' @param params A list of parameters to pass along with the cypher query +#' @param ... arguments to pass along to the call_neo4j function +#' +#' @return the result from the Neo4j Call +#' @export +neo4r_cypher <- function(con, query, params, ...) { + call_neo4j(query, con, params, ...) +} + # con <- neo4j_api$new(url = "http://localhost:7474/", user = "neo4j", password = "pouetpouet") # call_api(query = 'MATCH (n:user) RETURN COUNT (n) AS user_count;', con) diff --git a/man/call_neo4j.Rd b/man/call_neo4j.Rd index 684dc98..9cc2caf 100644 --- a/man/call_neo4j.Rd +++ b/man/call_neo4j.Rd @@ -4,14 +4,17 @@ \alias{call_neo4j} \title{Call Neo4J API} \usage{ -call_neo4j(query, con, type = c("row", "graph"), output = c("r", - "json"), include_stats = FALSE, include_meta = FALSE) +call_neo4j(query, con, params = list(), type = c("row", "graph"), + output = c("r", "json"), include_stats = FALSE, + include_meta = FALSE) } \arguments{ \item{query}{The cypher query} \item{con}{A NEO4JAPI connection object} +\item{params}{A list of parameters to pass along with the cypher query} + \item{type}{Return the result as row or as graph} \item{output}{Use "json" if you want the output to be printed as JSON} diff --git a/man/neo4j_api.Rd b/man/neo4j_api.Rd index e4c7a7d..b10b921 100644 --- a/man/neo4j_api.Rd +++ b/man/neo4j_api.Rd @@ -4,7 +4,7 @@ \name{neo4j_api} \alias{neo4j_api} \title{A Neo4J Connexion} -\format{An object of class \code{R6ClassGenerator} of length 24.} +\format{An object of class \code{R6ClassGenerator} of length 25.} \usage{ neo4j_api } diff --git a/man/neo4r_cypher.Rd b/man/neo4r_cypher.Rd new file mode 100644 index 0000000..5e38321 --- /dev/null +++ b/man/neo4r_cypher.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/call_api.R +\name{neo4r_cypher} +\alias{neo4r_cypher} +\title{Run A Cypher Query (but start with the connection object)} +\usage{ +neo4r_cypher(con, query, params, ...) +} +\arguments{ +\item{con}{A NEO4JAPI connection object} + +\item{query}{The cypher query} + +\item{params}{A list of parameters to pass along with the cypher query} + +\item{...}{arguments to pass along to the call_neo4j function} +} +\value{ +the result from the Neo4j Call +} +\description{ +Run A Cypher Query (but start with the connection object) +}