Nothing
      # nodbi helper functions
#### variables ####
# provide private environment,
# e.g. for initTransformers()
#
.nodbi <- new.env()
# jq script for extracting field names
#
# The unique function takes as input an array and
# produces an array of the same elements, in sorted
# order, with duplicates removed
#
# However, field names are generated for each
# document in the input, they are not deduplicated
# across documents. The latter would require jqr
# to be able to handle jq's 'inputs' and flag "-n".
jqFieldNames <- '[ path(..) | map(select(type == "string")) | join(".") ] | unique[] '
#' doc_wrap
#'
#' used with couchdb, elastic
#'
#' @keywords internal
#' @noRd
#'
doc_wrap <- function(..., indent = 0, width = getOption("width")) {
  x <- paste0(..., collapse = "")
  wrapped <- strwrap(x, indent = indent, exdent = indent + 5L, width = width)
  paste0(wrapped, collapse = "\n")
}
#' assert
#'
#' used across nodbi
#'
#' @keywords internal
#' @noRd
#'
assert <- function(x, y) {
  if (!is.null(x)) {
    if (!any(class(x) %in% y)) {
      stop(deparse(substitute(x)), " must be one of class: ",
           paste0(y, collapse = ", "), call. = FALSE)
    }
  }
}
#' pkgNeeded
#'
#' helps to manage database backends
#'
#' @keywords internal
#' @noRd
#'
pkgNeeded <- function(pkg, minV, minVneeded = TRUE) {
  if (!requireNamespace(pkg, quietly = TRUE)) {
    stop(
      "Package '", pkg,
      "' must be installed to use this function.",
      call. = FALSE
    )
  }
  minVavailable <- utils::packageVersion(pkg) >=
    package_version(minV)
  if (!minVavailable && minVneeded) {
    stop(
      "Package '", pkg,
      "' must be updated to at least version ",
      minV, " to use this function.",
      call. = FALSE
    )
  }
  return(minVavailable)
}
#' closeNodbiConnections
#'
#' ensure closing database connection(s)
#'
#' @keywords internal
#' @noRd
#'
closeNodbiConnections <- function(e) {
  # this function is called by .onLoad, .onUnload, and
  # from reg.finalizer in src_{sqlite,postgres,duckdb}
  # search environment for docdb_src connections
  objIsNodbiConnection <- sapply(
    ls(e), function(i)
      any(class(eval(parse(text = i))) == "docdb_src"),
    USE.NAMES = TRUE
  )
  if (!length(objIsNodbiConnection)) return(invisible(NULL))
  objIsNodbiConnection <- objIsNodbiConnection[objIsNodbiConnection]
  # disconnect helper function
  nodbiDisconnect <- function(objName) {
    # get duckdb driver information
    ddbdrv <- attr(eval(parse(text = objName))$con, "driver")
    # close valid, and also invalid connections such as
    # needed for DuckDB where then driver is not null
    if (DBI::dbIsValid(eval(parse(text = objName))$con) ||
        (!is.null(ddbdrv) && DBI::dbIsValid(ddbdrv))) {
      # disconnect and shutdown if needed
      res <- try(suppressWarnings(
        DBI::dbDisconnect(
          eval(parse(text = objName))$con,
          # duckdb needs to be shut down; parameter
          # does not adversely affect other backends
          shutdown = TRUE)),
        silent = TRUE)
      # inform user
      if (!inherits(res, "try-error") && res)
        message("nodbi: docdb_src '", objName, "' disconnected and shut down. ")
    }
  }
  # iterate over connections
  for (i in seq_along(objIsNodbiConnection)) {
    # get name of connection object
    objName <- names(objIsNodbiConnection[i])
    # run disconnect
    switch(
      # class is e.g., src_duckdb docdb_src
      class(eval(parse(text = objName)))[1],
      "src_duckdb" = nodbiDisconnect(objName),
      "src_sqlite" = nodbiDisconnect(objName),
      "src_postgres" = nodbiDisconnect(objName),
      NULL
    )
  }
}
#' .onLoad
#'
#' set up handler before database is accessed
#' this is triggered e.g. by session restart
#'
#' @keywords internal
#' @noRd
#'
.onLoad <- function(libname, pkgname) {
  # register closing our connections
  reg.finalizer(
    e = globalenv(),
    f = closeNodbiConnections,
    onexit = TRUE
  )
  # load javascript
  initTransformers()
}
#' .onUnload
#'
#' a session restart does not trigger this
#'
#' @keywords internal
#' @noRd
#'
.onUnload <- function(libpath) {
  closeNodbiConnections(e = globalenv())
}
#' initTransformers
#'
#' provide access to javascript functions and modules
#' stored in inst/js or subdir js of installed package
#'
#' https://cran.r-project.org/web/packages/V8/vignettes/npm.html
#'
#' purpose of javascript: transform mongo-like query into SQL
#'
#' @importFrom V8 v8 JS
#' @keywords internal
#' @noRd
#'
initTransformers <- function() {
  # early exit
  if (length(.nodbi)) return(NULL)
  # prepare V8, see ./inst/js/
  ct <- V8::v8()
  # get javascript
  ct$source(system.file("js/bundle.js", package = "nodbi"))
  # expects mdb to be db.user.find('{}')
  ct$assign("mongo2sql", V8::JS("function(mdb) {out = injs.convertToSQL(mdb); return out;}"))
  # assign into package private environment, see zzz.R
  assign("ct", ct, envir = .nodbi)
  # debug
  if (options()[["verbose"]]) {
    message("\nJS initiated\n")
  }
  # exit
  invisible(NULL)
}
#' digestFields
#'
#' takes input from "fields" and returns:
#' - longest common path for a minimum number of path elements
#'   for use with SELECT and possibly WHERE
#' - fields to be included
#' - fields to be excluded
#'
#' @importFrom stringi stri_match_all_regex
#' @keywords internal
#' @noRd
#'
digestFields <- function(f, q) {
  # check parameter
  if (is.null(f)) f <- "{}"
  f <- jsonlite::minify(f)
  # translate q into SQL query syntax using mongo2sql
  initTransformers()
  # - used:
  # $gt, $gte, $lt, $lte, $ne
  # $nin, $in, $regex,
  # $not, $and, $or, $nor
  # - not used:
  # $geoIntersects, $geoWithin,
  # $mod, $exists, $size, $nearSphere, $near
  # $text, $all, $where, $comment,
  # $meta, $slice, $elemMatch
  # stop if unused operators are in query
  usedOps <- c("$options", "$eq", "$gt", "$gte", "$lt", "$lte",
               "$ne", "$in", "$regex", "$and", "$or", "$nor")
  qOps <- stringi::stri_extract_all_regex(q, "(\\$[a-z]+)")[[1]]
  if (!all(is.na(qOps)) && !all(qOps %in% usedOps))  {
    stop("nodbi only supports: ", paste0(usedOps[-1], collapse = " / "),
         "; this was the query used: ", q)
  }
  sqlQ <- .nodbi$ct$call("mongo2sql", paste0("db.user.find(", q, ");"))
  # query mangling
  queryFields <- unique(stats::na.omit(stringi::stri_match_all_regex(
    sqlQ, '"([-@._\\w]+?)"')[[1]][, 2, drop = TRUE]))
  if (!length(queryFields) & q != "{}") stop(
    "Parameter 'query' did not reference any fields:\n", q)
  queryRootFields <- gsub("[.].*", "", queryFields)
  queryPaths <- character(0L)
  queryCondition <- character(0L)
  if (length(queryFields)) {
    # SELECT * FROM user WHERE <extract this>;
    queryCondition <- sub(".+? WHERE (.+);", "\\1", sqlQ)
    # "a.b" to "a"."b"
    queryCondition <- stringi::stri_replace_all_fixed(
      queryCondition, queryFields, gsub("[.]", '"."', queryFields),
      vectorize_all = FALSE
    )
    queryPaths <- gsub("[.]", '"."', queryFields)
    # = to ==
    queryCondition <- sub(" = ", " == ", queryCondition)
  }
  # fields mangling
  includeFields <- unique(stats::na.omit(stringi::stri_match_all_regex(
    f, '"([-@._\\w]+?)":[ ]*1')[[1]][, 2, drop = TRUE]))
  includeRootFields <- unique(gsub("[.].*", "", includeFields))
  includeRootFields <- includeRootFields[includeRootFields != "_id"]
  includeMaxCharFields <- sapply(
    includeFields, function(i) {
      if (!grepl(".", i, fixed = TRUE)) return(i)
      if (nchar(i) <= 63L) return(i)
      locDot <- substring(i, 1L, 63L) # 63L bytes is maximum for postgres
      # https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
      locDot <- stringi::stri_locate_last_fixed(locDot, ".")[1, "start", drop = TRUE]
      substring(i, 1L, locDot - 1L)
    }, USE.NAMES = FALSE)
  excludeFields <- unique(stats::na.omit(stringi::stri_match_all_regex(
    f, '"([-@._\\w]+?)":[ ]*0')[[1]][, 2, drop = TRUE]))
  fieldStrings <- unique(c(includeFields, excludeFields))
  # translate mongo query into jq script to filter and select:
  # {"$or": [
  #   {"email": {"$regex": "lacychen@conjurica.com"}},
  #   {"tags": {"$regex": "^duis$"}}
  # ]}
  #
  # ->
  #
  # def m1: . | (if (type == "array" or type == "object" or type == "string") and
  # length == 0 then null else (if type == "array" then (.[] | m1) else [.][] end) end);
  #
  # select(
  #   ([ .email ] | map( . | m1 | test("lacychen@conjurica.com") ) | any )
  #   or
  #   ([ .tags ] | map( . | m1 | test("^duis$") ) | any )
  # )
  queryJq <- gsub("'", '"', queryCondition)
  # ugly but robust
  for (i in queryPaths) {
    # - handle IN since this uses brackets around argument
    xtr <- stringi::stri_extract_all_regex(
      queryJq, paste0("(\"", i, "\") IN (\\(.+?\\))"))[[1]]
    if (!all(is.na(xtr))) {
      xtr <- stringi::stri_replace_all_regex(
        xtr, paste0("(\"", i, "\") IN \\((.+?)\\)"), "$2")[[1]]
      # split on comma after number or double quote, avoid splitting on comma in string
      xtr <- strsplit(gsub("([0-9\"]),", "\\1@", xtr), "@")[[1]]
      # recompose
      xtr <- paste0(" . == ", xtr, collapse = " or ")
      # insert
      queryJq <- stringi::stri_replace_all_regex(
        queryJq,
        paste0("(\"", i, "\") IN (\\(.+?\\))", # brackets for IN
               "( AND | NOT | OR |\\)*$)"),
        # select([ .friends | m1 | .id | m1 ] | map ( . > 1 ) | any )
        paste0(" ([ .", gsub('"[.]"', " | m1 | .", i), " ] ",
               "| map( . | m1 | (", xtr, " ) ) | any ) $3"),
        vectorize_all = FALSE
      )
    }
    # - default operator handling
    queryJq <- stringi::stri_replace_all_regex(
      queryJq,
      paste0("(\"", i, "\") ([INOTREGXP=!<>']+ .+?)",
             # no extra bracket here
             "( AND | NOT | OR |\\)*$)"),
      # select([ .friends | m1 | .id | m1 ] | map ( . > 1 ) | any )
      paste0(" ([ .", gsub('"[.]"', " | m1 | .", i), " ] ",
             "| map( . | m1 | . $2 ) | any ) $3"),
      vectorize_all = FALSE
    )
  }
  ## special cases
  # important
  queryJq <- gsub(" ==* ", " == ", queryJq)
  # https://jqlang.github.io/jq/manual/#test
  # https://jqlang.github.io/jq/manual/#regular-expressions
  queryJq <- gsub("REGEXP \"(.+?)\"", '| test("\\1")', queryJq)
  queryJq <- gsub("( AND | NOT | OR )", "\\L\\1", queryJq, perl = TRUE)
  # null is less than anything https://jqplay.org/s/w-kmDHvfMfqVt3z
  queryJq <- gsub(" . != ([^nul])", " . != null and . != \\1", queryJq)
  queryJq <- gsub(" . (<=?) ([^nul])", " . != null and . \\1 \\2", queryJq)
  # add function definition
  queryJq <- paste0('
    def m1: . | (if (type == "array" or type == "object" or type == "string") and
    length == 0 then null else (if type == "array" then (.[] | m1) else [.][] end) end);
    select(', queryJq, ')')
  # output
  return(list(
    # vector of fields
    includeFields = includeFields,
    includeRootFields = includeRootFields,
    includeMaxCharFields = includeMaxCharFields,
    excludeFields = excludeFields,
    queryRootFields = queryRootFields,
    queryFields = queryFields,
    queryPaths = queryPaths,
    queryCondition = queryCondition,
    queryJq = queryJq
  ))
}
#' insObj
#'
#' replaces names of objects within sql quotes
#' `/** **/` by contents of objects of that name
#' found in the calling environment. Also adds
#' brackets where found.
#'
#' @keywords internal
#' @noRd
#'
insObj <- function(x, p = parent.frame(), e = NULL) {
  x <- gsub("\n+", " ", x)
  x <- gsub("  +", " ", x)
  allFound <- stringi::stri_extract_all_regex(x, "(/[*][*].*?[*][*]/)", simplify = FALSE)[[1]]
  if (setequal(allFound, e)) return(x)
  if (all(is.na(allFound))) return(x)
  for (oneFound in unique(allFound)) {
    i <- stringi::stri_replace_all_fixed(oneFound, c("/**", "**/"), "", vectorize_all = FALSE)
    i <- trimws(i)
    b <- stringi::stri_extract_all_fixed(i, c("'", '"'), simplify = FALSE)
    b <- unique(stats::na.omit(unlist(b)))
    i <- gsub("'|\"", "", i)
    if (grepl("[$]", i)) { # handle list
      ii <- sub(".+[$](.+)", "\\1", i)
      c <- get(sub("(.+)[$].+", "\\1", i), envir = p)[[ii]]
    } else {
      c <- get(i, envir = p)
    }
    if (!is.null(c) && length(c)) {
      if (length(c) > 1L) stop(
        call. = FALSE,
        "Objects should be atomic character vectors, this is not: ", i)
      if (length(b)) c <- paste0(b, c, b, collapse = "")
      x <- stringi::stri_replace_all_fixed(x, oneFound, c)
    }
  }
  # recurse
  insObj(x = x, p = p, e = allFound)
}
# for use in query.R
utils::globalVariables("jqFieldNames")
# print database information
srcInfo <- function(srcSrc, srcVer, srcDb, srcSize) {
  if (!length(srcDb)) srcDb <- NA
  if (!length(srcSize)) srcSize <- NA
  cat(sprintf(
    "src: %s\nver: %s\ndb(s): %s\nsize(s): %s MB",
    srcSrc, srcVer,
    paste0(srcDb, collapse = " / "),
    paste0(signif(srcSize / 10^6, digits = 3L), collapse = " / ")
  ))
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.