Establishes version control for the personal Logseq DB fork before starting the server-client architecture migration and domain features described in docs/agent-guide and the approved implementation plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
953 lines
39 KiB
OCaml
953 lines
39 KiB
OCaml
type input_spec = {
|
|
name : string;
|
|
optional : bool;
|
|
default : Melange_edn_melange.any option;
|
|
}
|
|
|
|
type source = Built_in | Custom
|
|
|
|
type query_entry = {
|
|
name : string;
|
|
source : source;
|
|
doc : string option;
|
|
inputs : input_spec Rrbvec.t;
|
|
query : Melange_edn_melange.any;
|
|
}
|
|
|
|
type opts = {
|
|
query_edn : string option;
|
|
name : string option;
|
|
inputs_edn : string option;
|
|
}
|
|
|
|
type parsed = Parsed_run of opts | Parsed_list
|
|
|
|
type action =
|
|
| Run of {
|
|
repo : Cli_primitive.repo;
|
|
graph : Cli_primitive.graph;
|
|
query : Melange_edn_melange.any;
|
|
inputs : Melange_edn_melange.any Rrbvec.t;
|
|
name : string option;
|
|
}
|
|
| List
|
|
|
|
let edn_value_of_string ~label text =
|
|
try Ok (Melange_edn_melange.of_edn_string text)
|
|
with Melange_edn_melange.Parse_error _ ->
|
|
Error (Error.invalid_options ("invalid " ^ label ^ " edn"))
|
|
|
|
let normalize_query_name value =
|
|
let value = String.trim value in
|
|
if value = "" then None
|
|
else if String.length value > 0 && value.[0] = ':' then
|
|
Some (String.sub value 1 (String.length value - 1))
|
|
else Some value
|
|
|
|
let parsed_edn text =
|
|
match edn_value_of_string ~label:"built-in query" text with
|
|
| Ok value -> value
|
|
| Error err -> failwith err.message
|
|
|
|
let kw value = Edn_util.keyword value
|
|
let sym value = Edn_util.symbol value
|
|
let in_sym value = Melange_edn_melange.symbol value
|
|
let vector_vec values = Edn_util.vector_vec values
|
|
let vector_t_vec values = Edn_util.vector_t_vec values
|
|
let vector_t values = vector_t_vec values
|
|
let list_vec values = Edn_util.list_vec values
|
|
let list_t_vec values = Edn_util.list_t_vec values
|
|
let list_t values = list_t_vec values
|
|
let where_v values = Cli_primitive.V (vector_t values)
|
|
let where_l values = Cli_primitive.L (list_t values)
|
|
|
|
let query_value query =
|
|
Edn_util.any (Cli_primitive.datascript_query_to_edn query)
|
|
|
|
let input ?default name =
|
|
{ name; optional = String.length name > 0 && name.[0] = '?'; default }
|
|
|
|
let built_in_queries =
|
|
Vec.of_array
|
|
[|
|
|
{
|
|
name = "list-priority";
|
|
source = Built_in;
|
|
doc = Some "List closed values for the Priority property.";
|
|
inputs = Vec.empty;
|
|
query =
|
|
query_value
|
|
(Cli_primitive.make_datascript_query
|
|
~find:
|
|
(Vec.of_array
|
|
[|
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "pull";
|
|
sym "?value";
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
kw "db/id";
|
|
kw "db/ident";
|
|
kw "block/order";
|
|
|]);
|
|
|]);
|
|
sym "...";
|
|
|]);
|
|
|])
|
|
~where:
|
|
(Vec.of_array
|
|
[|
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
sym "?property";
|
|
kw "db/ident";
|
|
kw "logseq.property/priority";
|
|
|]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
sym "?value";
|
|
kw "block/closed-value-property";
|
|
sym "?property";
|
|
|]);
|
|
|])
|
|
());
|
|
};
|
|
{
|
|
name = "list-status";
|
|
source = Built_in;
|
|
doc = Some "List closed values for the Status property.";
|
|
inputs = Vec.empty;
|
|
query =
|
|
query_value
|
|
(Cli_primitive.make_datascript_query
|
|
~find:
|
|
(Vec.of_array
|
|
[|
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "pull";
|
|
sym "?value";
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
kw "db/id";
|
|
kw "db/ident";
|
|
kw "block/order";
|
|
|]);
|
|
|]);
|
|
sym "...";
|
|
|]);
|
|
|])
|
|
~where:
|
|
(Vec.of_array
|
|
[|
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
sym "?property";
|
|
kw "db/ident";
|
|
kw "logseq.property/status";
|
|
|]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
sym "?value";
|
|
kw "block/closed-value-property";
|
|
sym "?property";
|
|
|]);
|
|
|])
|
|
());
|
|
};
|
|
{
|
|
name = "recent-updated";
|
|
source = Built_in;
|
|
doc = Some "Find entities updated within recent-days.";
|
|
inputs = Vec.of_array [| input "recent-days"; input "?now-ms" |];
|
|
query =
|
|
query_value
|
|
(Cli_primitive.make_datascript_query
|
|
~find:
|
|
(Vec.of_array
|
|
[|
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "pull";
|
|
sym "?e";
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
kw "db/id";
|
|
kw "block/title";
|
|
kw "block/updated-at";
|
|
|]);
|
|
|]);
|
|
sym "...";
|
|
|]);
|
|
|])
|
|
~in_:
|
|
(Vec.of_array
|
|
[| in_sym "$"; in_sym "?recent-days"; in_sym "?now-ms" |])
|
|
~where:
|
|
(Vec.of_array
|
|
[|
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
sym "?e"; kw "block/updated-at"; sym "?updated-at";
|
|
|]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "missing?";
|
|
sym "$";
|
|
sym "?e";
|
|
kw "logseq.property/built-in?";
|
|
|]);
|
|
|]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "*";
|
|
sym "?recent-days";
|
|
Edn_util.int 86400000;
|
|
|]);
|
|
sym "?recent-days-ms";
|
|
|]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "-";
|
|
sym "?now-ms";
|
|
sym "?recent-days-ms";
|
|
|]);
|
|
sym "?days-ago";
|
|
|]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym ">="; sym "?updated-at"; sym "?days-ago";
|
|
|]);
|
|
|]);
|
|
|])
|
|
());
|
|
};
|
|
{
|
|
name = "task-search";
|
|
source = Built_in;
|
|
doc =
|
|
Some
|
|
"Find tasks by status, optional title substring, optional \
|
|
recent-days.";
|
|
inputs =
|
|
Vec.of_array
|
|
[|
|
|
input "search-status";
|
|
input ~default:(Edn_util.string "") "?search-title";
|
|
input ~default:(Edn_util.int 0) "?recent-days";
|
|
input "?now-ms";
|
|
|];
|
|
query =
|
|
query_value
|
|
(Cli_primitive.make_datascript_query
|
|
~find:
|
|
(Vec.of_array
|
|
[|
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "pull";
|
|
sym "?e";
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
kw "db/id";
|
|
kw "block/title";
|
|
kw "block/updated-at";
|
|
|]);
|
|
|]);
|
|
sym "...";
|
|
|]);
|
|
|])
|
|
~in_:
|
|
(Vec.of_array
|
|
[|
|
|
in_sym "$";
|
|
in_sym "?search-status";
|
|
in_sym "?search-title";
|
|
in_sym "?recent-days";
|
|
in_sym "?now-ms";
|
|
|])
|
|
~where:
|
|
(Vec.of_array
|
|
[|
|
|
where_v
|
|
(Vec.of_array
|
|
[| sym "?e"; kw "block/title"; sym "?title" |]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
sym "?e";
|
|
kw "logseq.property/status";
|
|
sym "?status";
|
|
|]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
sym "?status"; kw "db/ident"; sym "?status-ident";
|
|
|]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "=";
|
|
sym "?status-ident";
|
|
sym "?search-status";
|
|
|]);
|
|
|]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "clojure.string/lower-case";
|
|
sym "?title";
|
|
|]);
|
|
sym "?title-lower-case";
|
|
|]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[| sym "str"; sym "?search-title" |]);
|
|
sym "?search-title-string";
|
|
|]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "clojure.string/lower-case";
|
|
sym "?search-title-string";
|
|
|]);
|
|
sym "?search-title-lower-case";
|
|
|]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "clojure.string/includes?";
|
|
sym "?title-lower-case";
|
|
sym "?search-title-lower-case";
|
|
|]);
|
|
|]);
|
|
where_v
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "get-else";
|
|
sym "$";
|
|
sym "?e";
|
|
kw "block/updated-at";
|
|
Edn_util.int 0;
|
|
|]);
|
|
sym "?updated-at";
|
|
|]);
|
|
where_l
|
|
(Vec.of_array
|
|
[|
|
|
sym "or-join";
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "?recent-days";
|
|
sym "?updated-at";
|
|
sym "?now-ms";
|
|
sym "?days-ago";
|
|
|]);
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "and";
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "nil?"; sym "?recent-days";
|
|
|]);
|
|
|]);
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "identity"; Edn_util.int 0;
|
|
|]);
|
|
sym "?days-ago";
|
|
|]);
|
|
|]);
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "and";
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "<=";
|
|
sym "?recent-days";
|
|
Edn_util.int 0;
|
|
|]);
|
|
|]);
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "identity"; Edn_util.int 0;
|
|
|]);
|
|
sym "?days-ago";
|
|
|]);
|
|
|]);
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "and";
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "*";
|
|
sym "?recent-days";
|
|
Edn_util.int 86400000;
|
|
|]);
|
|
sym "?recent-days-ms";
|
|
|]);
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym "-";
|
|
sym "?now-ms";
|
|
sym "?recent-days-ms";
|
|
|]);
|
|
sym "?days-ago";
|
|
|]);
|
|
vector_vec
|
|
(Vec.of_array
|
|
[|
|
|
list_vec
|
|
(Vec.of_array
|
|
[|
|
|
sym ">=";
|
|
sym "?updated-at";
|
|
sym "?days-ago";
|
|
|]);
|
|
|]);
|
|
|]);
|
|
|]);
|
|
|])
|
|
());
|
|
};
|
|
|]
|
|
|> Vec.sort (fun (a : query_entry) (b : query_entry) ->
|
|
String.compare a.name b.name)
|
|
|
|
let db_query_dsl_rules =
|
|
parsed_edn
|
|
{|[
|
|
[(parent ?p ?c) [?c :block/parent ?p]]
|
|
[(parent ?p ?c) [?t :block/parent ?p] (parent ?t ?c)]
|
|
[(class-extends ?p ?c) [?c :logseq.property.class/extends ?p]]
|
|
[(class-extends ?p ?c) [?t :logseq.property.class/extends ?p] (class-extends ?t ?c)]
|
|
[(alias ?e2 ?e1) [?e2 :block/alias ?e1]]
|
|
[(alias ?e2 ?e1) [?e1 :block/alias ?e2]]
|
|
[(self-ref ?b ?ref) [?b :block/refs ?ref]]
|
|
[(has-ref ?b ?r) [?b :block/refs ?r]]
|
|
[(has-ref ?b ?r) (parent ?p ?b) [?p :block/refs ?r]]
|
|
[(page-ref ?b ?ref) (has-ref ?b ?ref)]
|
|
[(block-content ?b ?query) [?b :block/title ?content] [(clojure.string/includes? ?content ?query)]]
|
|
[(page ?b ?page-name) [?b :block/page ?bp] [?bp :block/name ?page-name]]
|
|
[(between ?b ?start ?end) [?b :block/page ?p] [?p :block/tags :logseq.class/Journal] [?p :block/journal-day ?d] [(>= ?d ?start)] [(<= ?d ?end)]]
|
|
[(ref->val ?pv ?val) [?pv :block/title ?val]]
|
|
[(ref->val ?pv ?val) [?pv :logseq.property/value ?val]]
|
|
[(property-missing-value ?b ?prop-e ?default-p ?default-v) [?t :logseq.property.class/properties ?prop-e] [?prop-e :db/ident ?prop] (object-has-class-property? ?b ?prop) [(get-else $ ?b ?prop "N/A") ?prop-v] [(= ?prop-v "N/A")] [?prop-e ?default-p ?default-v]]
|
|
[(scalar-property-value ?b ?prop-e ?val) [?prop-e :db/ident ?prop] [?b ?prop ?val]]
|
|
[(scalar-property-value-with-default ?b ?prop-e ?val) (scalar-property-value ?b ?prop-e ?val)]
|
|
[(scalar-property-value-with-default ?b ?prop-e ?val) (property-missing-value ?b ?prop-e :logseq.property/scalar-default-value ?val)]
|
|
[(ref-property-value ?b ?prop-e ?val) [?prop-e :db/ident ?prop] [?b ?prop ?pv] (ref->val ?pv ?val)]
|
|
[(ref-property-value-with-default ?b ?prop-e ?val) (ref-property-value ?b ?prop-e ?val)]
|
|
[(ref-property-value-with-default ?b ?prop-e ?val) (property-missing-value ?b ?prop-e :logseq.property/default-value ?pv) (ref->val ?pv ?val)]
|
|
[(object-has-class-property? ?b ?prop) [?prop-e :db/ident ?prop] [?t :logseq.property.class/properties ?prop-e] [?b :block/tags ?tc] (or [(= ?t ?tc)] (class-extends ?t ?tc))]
|
|
[(has-property-or-object-property? ?b ?prop) [?prop-e :db/ident ?prop] (or [?b ?prop _] (object-has-class-property? ?b ?prop))]
|
|
[(has-simple-query-property ?b ?prop) [?prop-e :db/ident ?prop] [?prop-e :block/tags :logseq.class/Property] (has-property-or-object-property? ?b ?prop) (or [(missing? $ ?prop-e :logseq.property/public?)] [?prop-e :logseq.property/public? true])]
|
|
[(has-private-simple-query-property ?b ?prop) [?prop-e :db/ident ?prop] [?prop-e :block/tags :logseq.class/Property] (has-property-or-object-property? ?b ?prop)]
|
|
[(has-property ?b ?prop) [?b ?prop _] [?prop-e :db/ident ?prop] [?prop-e :block/tags :logseq.class/Property] (or [(missing? $ ?prop-e :logseq.property/public?)] [?prop-e :logseq.property/public? true])]
|
|
[(scalar-property ?b ?prop ?val) [?prop-e :db/ident ?prop] (scalar-property-value ?b ?prop-e ?val) (or [(missing? $ ?prop-e :logseq.property/public?)] [?prop-e :logseq.property/public? true])]
|
|
[(scalar-property-with-default ?b ?prop ?val) [?prop-e :db/ident ?prop] (scalar-property-value-with-default ?b ?prop-e ?val) (or [(missing? $ ?prop-e :logseq.property/public?)] [?prop-e :logseq.property/public? true])]
|
|
[(ref-property ?b ?prop ?val) [?prop-e :db/ident ?prop] (ref-property-value ?b ?prop-e ?val) (or [(missing? $ ?prop-e :logseq.property/public?)] [?prop-e :logseq.property/public? true])]
|
|
[(ref-property-with-default ?b ?prop ?val) [?prop-e :db/ident ?prop] (ref-property-value-with-default ?b ?prop-e ?val) (or [(missing? $ ?prop-e :logseq.property/public?)] [?prop-e :logseq.property/public? true])]
|
|
[(private-scalar-property ?b ?prop ?val) [?prop-e :db/ident ?prop] [?prop-e :block/tags :logseq.class/Property] (scalar-property-value ?b ?prop-e ?val)]
|
|
[(private-scalar-property-with-default ?b ?prop ?val) [?prop-e :db/ident ?prop] [?prop-e :block/tags :logseq.class/Property] (scalar-property-value-with-default ?b ?prop-e ?val)]
|
|
[(private-ref-property ?b ?prop ?val) [?prop-e :db/ident ?prop] [?prop-e :block/tags :logseq.class/Property] (ref-property-value ?b ?prop-e ?val)]
|
|
[(private-ref-property-with-default ?b ?prop ?val) [?prop-e :db/ident ?prop] [?prop-e :block/tags :logseq.class/Property] (ref-property-value-with-default ?b ?prop-e ?val)]
|
|
[(property ?b ?prop ?val) [?prop-e :db/ident ?prop] [?prop-e :block/tags :logseq.class/Property] (or [(missing? $ ?prop-e :logseq.property/public?)] [?prop-e :logseq.property/public? true]) [?b ?prop ?pv] (or (and [(missing? $ ?prop-e :db/valueType)] [?b ?prop ?val]) (and [?prop-e :db/valueType :db.type/ref] (or [?pv :block/title ?val] [?pv :logseq.property/value ?val])))]
|
|
[(tag-spec->tag ?tag ?spec) [(number? ?spec)] [(identity ?spec) ?tag]]
|
|
[(tag-spec->tag ?tag ?spec) [?tag :block/title ?spec]]
|
|
[(tag-spec->tag ?tag ?spec) [?tag :db/ident ?spec]]
|
|
[(tags ?b ?tags) [(identity ?tags) [?spec ...]] (tag-spec->tag ?tag ?spec) [?b :block/tags ?tc] (or [(= ?tag ?tc)] (class-extends ?tag ?tc)) [(missing? $ ?b :block/link)]]
|
|
[(task ?b ?statuses) (ref-property-with-default ?b :logseq.property/status ?val) [(contains? ?statuses ?val)]]
|
|
[(priority ?b ?priorities) (ref-property-with-default ?b :logseq.property/priority ?priority) [(contains? ?priorities ?priority)]]
|
|
]|}
|
|
|
|
let trim_non_empty value =
|
|
let value = String.trim value in
|
|
if value = "" then None else Some value
|
|
|
|
let strip_leading_colon value =
|
|
if String.length value > 0 && value.[0] = ':' then
|
|
String.sub value 1 (String.length value - 1)
|
|
else value
|
|
|
|
let query_name_of_value value =
|
|
match (Edn_util.as_string value, Edn_util.as_keyword value) with
|
|
| Some value, _ | _, Some value -> trim_non_empty (strip_leading_colon value)
|
|
| _ ->
|
|
trim_non_empty
|
|
(Melange_edn_melange.to_edn_string value |> strip_leading_colon)
|
|
|
|
let custom_query_input value =
|
|
match (Edn_util.as_string value, Edn_util.as_keyword value) with
|
|
| Some value, _ | _, Some value ->
|
|
Option.map
|
|
(fun name -> input name)
|
|
(trim_non_empty (strip_leading_colon value))
|
|
| _ -> (
|
|
match Edn_util.as_map value with
|
|
| Some _ -> (
|
|
match Option.bind (Edn_util.get value "name") query_name_of_value with
|
|
| None -> None
|
|
| Some name ->
|
|
Some
|
|
{
|
|
name;
|
|
optional = String.length name > 0 && name.[0] = '?';
|
|
default = Edn_util.get value "default";
|
|
})
|
|
| None -> None)
|
|
|
|
let custom_query_inputs = function
|
|
| Some value -> (
|
|
match Edn_util.as_seq value with
|
|
| Some values -> Vec.filter_map custom_query_input values
|
|
| _ -> Vec.empty)
|
|
| _ -> Vec.empty
|
|
|
|
let custom_query_entry name source spec =
|
|
let query, doc, inputs =
|
|
match spec with
|
|
| spec -> (
|
|
match (Edn_util.as_vector spec, Edn_util.as_map spec) with
|
|
| Some _, _ -> (Some spec, None, Vec.empty)
|
|
| _, Some _ ->
|
|
( Edn_util.get spec "query",
|
|
Edn_util.get_string spec "doc",
|
|
custom_query_inputs (Edn_util.get spec "inputs") )
|
|
| _ -> (None, None, Vec.empty))
|
|
in
|
|
match (query_name_of_value name, query) with
|
|
| Some name, Some query -> Some { name; source; doc; inputs; query }
|
|
| _ -> None
|
|
|
|
let custom_queries config =
|
|
match
|
|
Option.bind config.Cli_config.raw_file_config (fun value ->
|
|
Edn_util.get value "custom-queries")
|
|
with
|
|
| Some value -> (
|
|
match Edn_util.as_map value with
|
|
| Some fields ->
|
|
Vec.filter_map
|
|
(fun (name, spec) -> custom_query_entry name Custom spec)
|
|
fields
|
|
| _ -> Vec.empty)
|
|
| _ -> Vec.empty
|
|
|
|
let merge_queries entries =
|
|
let upsert acc (entry : query_entry) =
|
|
acc
|
|
|> Vec.filter (fun (existing : query_entry) -> existing.name <> entry.name)
|
|
|> fun acc -> Vec.push_back acc entry
|
|
in
|
|
entries
|
|
|> Vec.fold_left upsert Vec.empty
|
|
|> Vec.sort (fun (a : query_entry) (b : query_entry) ->
|
|
String.compare a.name b.name)
|
|
|
|
let list_queries config : query_entry Rrbvec.t =
|
|
merge_queries (Vec.append built_in_queries (custom_queries config))
|
|
|
|
let find_query config name =
|
|
Vec.find_opt
|
|
(fun (entry : query_entry) -> entry.name = name)
|
|
(list_queries config)
|
|
|
|
let rec contains_db_id_datom_clause value =
|
|
match (Edn_util.as_vector value, Edn_util.as_list value) with
|
|
| Some values, _
|
|
when Vec.length values >= 2
|
|
&& Edn_util.as_keyword (Vec.nth values 1) = Some "db/id" ->
|
|
true
|
|
| Some values, _ | _, Some values ->
|
|
Vec.exists contains_db_id_datom_clause values
|
|
| _ -> false
|
|
|
|
let validate_query query =
|
|
match Edn_util.as_vector query with
|
|
| Some values ->
|
|
let rec after_where values =
|
|
match Vec.pop_front values with
|
|
| None -> Vec.empty
|
|
| Some (keyword, clauses)
|
|
when Edn_util.as_keyword keyword = Some "where" ->
|
|
clauses
|
|
| Some (_, rest) -> after_where rest
|
|
in
|
|
if Vec.exists contains_db_id_datom_clause (after_where values) then
|
|
Error
|
|
(Error.make Error.Invalid_query
|
|
"invalid query: :db/id cannot be used as a datom attribute in \
|
|
:where clauses. Bind entity ids through :in and --inputs.")
|
|
else Ok query
|
|
| _ -> Error (Error.invalid_options "query must be a vector")
|
|
|
|
let current_epoch_ms () = Time.time_to_epoch_ms (Time.now ())
|
|
|
|
let normalize_task_search_inputs (entry : query_entry option) inputs =
|
|
match entry with
|
|
| Some { name = "task-search"; _ } -> (
|
|
match Vec.pop_front inputs with
|
|
| Some (status_value, rest)
|
|
when Option.is_some (Edn_util.as_string status_value) ->
|
|
let status =
|
|
Option.value (Edn_util.as_string status_value) ~default:""
|
|
in
|
|
let status = String.trim status |> String.lowercase_ascii in
|
|
if status = "" then inputs
|
|
else
|
|
Vec.push_front rest
|
|
(Edn_util.keyword ("logseq.property/status." ^ status))
|
|
| _ -> inputs)
|
|
| _ -> inputs
|
|
|
|
let validate_recent_updated_inputs (entry : query_entry option) inputs =
|
|
match (entry, Vec.peek_front_opt inputs) with
|
|
| ( Some { name = "recent-updated"; source = Built_in; _ },
|
|
Some recent_days_value )
|
|
when Option.value (Edn_util.as_int recent_days_value) ~default:0 > 0 ->
|
|
Ok inputs
|
|
| Some { name = "recent-updated"; source = Built_in; _ }, _ ->
|
|
Error (Error.invalid_options "recent-days must be a positive integer")
|
|
| _ -> Ok inputs
|
|
|
|
let normalize_inputs entry inputs =
|
|
match entry with
|
|
| None -> Ok inputs
|
|
| Some entry ->
|
|
let required =
|
|
Vec.length (Vec.filter (fun spec -> not spec.optional) entry.inputs)
|
|
in
|
|
if Vec.length inputs < required then
|
|
Error (Error.invalid_options "inputs missing required values")
|
|
else
|
|
let rec fill acc specs values =
|
|
match Vec.pop_front specs with
|
|
| Some (_spec, rest_specs) when not (Vec.is_empty values) ->
|
|
let value, rest_values =
|
|
match Vec.pop_front values with
|
|
| Some pair -> pair
|
|
| None -> failwith "non-empty vector must have a front value"
|
|
in
|
|
fill (Vec.push_back acc value) rest_specs rest_values
|
|
| None -> Vec.append acc values
|
|
| Some (spec, rest) ->
|
|
let default =
|
|
match spec.default with
|
|
| Some value when Edn_util.as_keyword value = Some "now-ms" ->
|
|
Edn_util.int64 (current_epoch_ms ())
|
|
| Some value -> value
|
|
| None when spec.name = "?now-ms" ->
|
|
Edn_util.int64 (current_epoch_ms ())
|
|
| None -> Edn_util.nil
|
|
in
|
|
fill (Vec.push_back acc default) rest Vec.empty
|
|
in
|
|
fill Vec.empty entry.inputs inputs
|
|
|> normalize_task_search_inputs (Some entry)
|
|
|> validate_recent_updated_inputs (Some entry)
|
|
|
|
let command_id = function
|
|
| Parsed_run _ -> Command_id.Query
|
|
| Parsed_list -> Query_list
|
|
|
|
let validate_parsed _ = Ok ()
|
|
|
|
let symbol_is name = function
|
|
| Melange_edn_melange.Any (Melange_edn_melange.Symbol value) -> value = name
|
|
| _ -> false
|
|
|
|
let query_in_ends_with_percent query =
|
|
match Edn_util.as_vector query with
|
|
| Some values -> (
|
|
let rec find_in values =
|
|
match Vec.pop_front values with
|
|
| None -> Vec.empty
|
|
| Some (keyword, rest) when Edn_util.as_keyword keyword = Some "in" ->
|
|
collect_in Vec.empty rest
|
|
| Some (_, rest) -> find_in rest
|
|
and collect_in acc values =
|
|
match Vec.pop_front values with
|
|
| None -> acc
|
|
| Some (keyword, _) when Option.is_some (Edn_util.as_keyword keyword) ->
|
|
acc
|
|
| Some (value, rest) -> collect_in (Vec.push_back acc value) rest
|
|
in
|
|
match find_in values with
|
|
| values when not (Vec.is_empty values) ->
|
|
symbol_is "%" (Vec.nth values (Vec.length values - 1))
|
|
| _ -> false)
|
|
| _ -> false
|
|
|
|
let query_args query inputs =
|
|
let args = Vec.push_front inputs query in
|
|
if query_in_ends_with_percent query then Vec.push_back args db_query_dsl_rules
|
|
else args
|
|
|
|
let build ?registry:_ config _globals parsed =
|
|
match parsed with
|
|
| Parsed_list -> Ok List
|
|
| Parsed_run opts -> (
|
|
match config.Cli_config.repo with
|
|
| None -> Error (Error.missing_repo "repo is required for query")
|
|
| Some repo ->
|
|
let query_text = Option.bind opts.query_edn Cli_primitive.non_empty in
|
|
let query_name = Option.bind opts.name normalize_query_name in
|
|
if Option.is_some query_text && Option.is_some query_name then
|
|
Error
|
|
(Error.invalid_options "use either --query or --name, not both")
|
|
else if Option.is_none query_text && Option.is_none query_name then
|
|
Error (Error.make Error.Missing_query "query is required")
|
|
else
|
|
let query_result, entry =
|
|
match (query_text, query_name) with
|
|
| Some text, _ -> (edn_value_of_string ~label:"query" text, None)
|
|
| None, Some name -> (
|
|
match find_query config name with
|
|
| Some entry -> (Ok entry.query, Some entry)
|
|
| None ->
|
|
( Error
|
|
(Error.make Error.Unknown_query
|
|
("unknown query: " ^ name)),
|
|
None ))
|
|
| _ ->
|
|
( Error (Error.make Error.Missing_query "query is required"),
|
|
None )
|
|
in
|
|
Error.bind query_result (fun query ->
|
|
Error.bind (validate_query query) (fun query ->
|
|
let inputs_result =
|
|
match
|
|
Option.bind opts.inputs_edn Cli_primitive.non_empty
|
|
with
|
|
| None -> Ok Vec.empty
|
|
| Some text -> (
|
|
match edn_value_of_string ~label:"inputs" text with
|
|
| Ok value -> (
|
|
match Edn_util.as_vector value with
|
|
| Some inputs -> Ok inputs
|
|
| _ ->
|
|
Error
|
|
(Error.invalid_options
|
|
"inputs must be a vector"))
|
|
| Error _ as err -> err)
|
|
in
|
|
Error.bind inputs_result (fun inputs ->
|
|
Error.bind (normalize_inputs entry inputs)
|
|
(fun inputs ->
|
|
Ok
|
|
(Run
|
|
{
|
|
repo;
|
|
graph = Cli_config.repo_to_graph repo;
|
|
query;
|
|
inputs;
|
|
name = query_name;
|
|
}))))))
|
|
|
|
let source_value = function
|
|
| Built_in -> Edn_util.string "built-in"
|
|
| Custom -> Edn_util.string "custom"
|
|
|
|
let input_value (input : input_spec) =
|
|
let fields =
|
|
Vec.of_array
|
|
[|
|
|
(Edn_util.keyword "name", Edn_util.string input.name);
|
|
(Edn_util.keyword "optional", Edn_util.bool input.optional);
|
|
|]
|
|
|> fun fields ->
|
|
match input.default with
|
|
| Some value -> Vec.push_back fields (Edn_util.keyword "default", value)
|
|
| None -> fields
|
|
in
|
|
Edn_util.map_vec fields
|
|
|
|
let hide_internal_inputs (entry : query_entry) : query_entry =
|
|
{
|
|
entry with
|
|
inputs =
|
|
Vec.filter
|
|
(fun (input : input_spec) -> input.name <> "?now-ms")
|
|
entry.inputs;
|
|
}
|
|
|
|
let query_entry_value (entry : query_entry) =
|
|
let entry = hide_internal_inputs entry in
|
|
Edn_util.map_vec
|
|
(Vec.of_array
|
|
[|
|
|
(Edn_util.keyword "name", Edn_util.string entry.name);
|
|
(Edn_util.keyword "source", source_value entry.source);
|
|
( Edn_util.keyword "doc",
|
|
match entry.doc with
|
|
| Some doc -> Edn_util.string doc
|
|
| None -> Edn_util.nil );
|
|
( Edn_util.keyword "inputs",
|
|
Edn_util.vector_vec (entry.inputs |> Vec.map input_value) );
|
|
(Edn_util.keyword "query", entry.query);
|
|
|])
|
|
|
|
let execute_with_mode action config mode =
|
|
let open Cli_effect in
|
|
match action with
|
|
| List ->
|
|
pure
|
|
(Cli_result.ok ~command:Command_id.Query_list mode
|
|
(Raw
|
|
(Edn_util.map_vec
|
|
(Vec.of_array
|
|
[|
|
|
( Edn_util.keyword "queries",
|
|
Edn_util.vector_vec
|
|
(list_queries config |> Vec.map query_entry_value) );
|
|
|]))))
|
|
| Run { repo; query; inputs; _ } ->
|
|
bind (Server_runtime.ensure_server config repo ~create_empty_db:false)
|
|
(function
|
|
| Error err ->
|
|
pure (Output_mode.error ~command:Command_id.Query mode err)
|
|
| Ok invoke_config ->
|
|
bind
|
|
(Transport.thread_api_q invoke_config ~repo
|
|
~query:(Edn_util.vector_t_vec (query_args query inputs)))
|
|
(fun value ->
|
|
pure
|
|
(Cli_result.ok ~command:Command_id.Query mode
|
|
(Query_result
|
|
(Edn_util.map_vec
|
|
(Vec.of_array
|
|
[| (Edn_util.keyword "result", value) |]))))))
|
|
|
|
let meta ?(examples = Vec.empty) id doc =
|
|
{
|
|
Command_registry.id;
|
|
path = Command_id.to_path id;
|
|
doc;
|
|
long_doc = None;
|
|
examples;
|
|
options = Vec.empty;
|
|
category = Command_registry.Graph_inspect_and_edit;
|
|
requires_graph = Command_id.requires_graph id;
|
|
requires_auth = Command_id.requires_auth id;
|
|
write_command = Command_id.is_write id;
|
|
human_table_headers_order = Vec.empty;
|
|
}
|
|
|
|
let metadata () =
|
|
Vec.of_array
|
|
[|
|
|
meta
|
|
~examples:
|
|
(Vec.of_array
|
|
[|
|
|
"logseq query --graph my-graph --name recent-updated --inputs \
|
|
'[30]'";
|
|
"logseq query --graph my-graph --name task-search --inputs \
|
|
'[:logseq.property/status.done \"daily\"]'";
|
|
"logseq query --graph my-graph --query '[:find [?e ...] :where \
|
|
[?e :block/name]]'";
|
|
|])
|
|
Command_id.Query "Run a Datascript query";
|
|
meta
|
|
~examples:
|
|
(Vec.of_array
|
|
[|
|
|
"logseq query list --graph my-graph";
|
|
"logseq query list --graph my-graph --output edn";
|
|
|])
|
|
Query_list "List available queries";
|
|
|]
|
|
|
|
let execute action config =
|
|
let (Output.Mode.Packed mode) = Output_mode.for_config config in
|
|
execute_with_mode action config mode
|