Files
Synapse/cli/lib/upsert.ml
Dasemu 40631362b3 Initial import of LogDarioSeq baseline
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>
2026-08-25 15:32:15 +02:00

3225 lines
114 KiB
OCaml

type mode = Create | Update | Page
type block_opts = {
id : Cli_primitive.db_id option;
uuid : Cli_primitive.uuid option;
target_id : Cli_primitive.db_id option;
target_uuid : Cli_primitive.uuid option;
target_page : string option;
pos : Block.position option;
content : string option;
blocks_edn : string option;
blocks_file : Cli_primitive.path option;
update_tags_edn : string option;
update_properties_edn : string option;
remove_tags_edn : string option;
remove_properties_edn : string option;
}
type page_opts = {
id : Cli_primitive.db_id option;
page : string option;
restore : bool;
update_tags_edn : string option;
update_properties_edn : string option;
remove_tags_edn : string option;
remove_properties_edn : string option;
}
type task_opts = {
id : Cli_primitive.db_id option;
uuid : Cli_primitive.uuid option;
page : string option;
content : string option;
target_id : Cli_primitive.db_id option;
target_uuid : Cli_primitive.uuid option;
target_page : string option;
pos : Block.position option;
status : string option;
priority : string option;
scheduled : Js.Date.t option;
deadline : Js.Date.t option;
no_status : bool;
no_priority : bool;
no_scheduled : bool;
no_deadline : bool;
}
type asset_opts = {
id : Cli_primitive.db_id option;
uuid : Cli_primitive.uuid option;
path : Cli_primitive.path option;
target_id : Cli_primitive.db_id option;
target_uuid : Cli_primitive.uuid option;
target_page : string option;
pos : Block.position option;
content : string option;
}
type tag_opts = {
id : Cli_primitive.db_id option;
name : string option;
add_properties_edn : string option;
remove_properties_edn : string option;
}
type property_opts = {
id : Cli_primitive.db_id option;
name : string option;
kind : Property.kind option;
cardinality : Property.cardinality option;
hide : bool option;
public : bool option;
}
type parsed =
| Parsed_block of block_opts
| Parsed_page of page_opts
| Parsed_task of task_opts
| Parsed_asset of asset_opts
| Parsed_tag of tag_opts
| Parsed_property of property_opts
type block_target =
| Target_id of Cli_primitive.db_id
| Target_uuid of Cli_primitive.uuid
| Target_page of string
type block_source =
| Source_id of Cli_primitive.db_id
| Source_uuid of Cli_primitive.uuid
type block_create = {
repo : Cli_primitive.repo;
graph : Cli_primitive.graph;
target : block_target;
pos : Block.position;
status : Cli_primitive.keyword option;
tags : Selector.tag Rrbvec.t;
properties : Property.assignment Rrbvec.t;
blocks : Block.t Rrbvec.t;
update_plan : Property.update_plan;
}
type block_update = {
repo : Cli_primitive.repo;
graph : Cli_primitive.graph;
source : block_source;
target : block_target option;
pos : Block.position option;
update_tags : Selector.tag Rrbvec.t;
update_properties : Property.assignment Rrbvec.t;
remove_tags : Selector.tag Rrbvec.t;
remove_properties : Property.key Rrbvec.t;
content : string option;
source_label : string option;
target_label : string option;
}
type block_action =
| Block_create of block_create
| Block_update of block_update
type action =
| Upsert_block of block_action
| Upsert_page of {
repo : Cli_primitive.repo;
graph : Cli_primitive.graph;
mode : mode;
id : Cli_primitive.db_id option;
page : string option;
restore : bool;
plan : Property.update_plan;
}
| Upsert_task of {
repo : Cli_primitive.repo;
graph : Cli_primitive.graph;
mode : mode;
id : Cli_primitive.db_id option;
uuid : Cli_primitive.uuid option;
page : string option;
content : string option;
update_properties : Property.assignment Rrbvec.t;
clear_properties : Property.key Rrbvec.t;
status_input : string option;
}
| Upsert_asset of {
repo : Cli_primitive.repo;
graph : Cli_primitive.graph;
mode : mode;
id : Cli_primitive.db_id option;
uuid : Cli_primitive.uuid option;
path : Cli_primitive.path option;
content : string option;
create_action : block_create option;
}
| Upsert_tag of {
repo : Cli_primitive.repo;
graph : Cli_primitive.graph;
mode : mode;
id : Cli_primitive.db_id option;
name : string option;
add_properties : Property.key Rrbvec.t;
remove_properties : Property.key Rrbvec.t;
}
| Upsert_property of {
repo : Cli_primitive.repo;
graph : Cli_primitive.graph;
mode : mode;
id : Cli_primitive.db_id option;
name : string option;
schema : Property.schema;
}
let update_mode (opts : block_opts) =
Option.is_some opts.id || Option.is_some opts.uuid
let normalize_tag_name value =
let value = String.trim value in
let rec drop_hash idx =
if idx < String.length value && value.[idx] = '#' then drop_hash (idx + 1)
else idx
in
let start = drop_hash 0 in
let value =
String.sub value start (String.length value - start) |> String.trim
in
if value = "" then None else Some value
let normalized_lookup_name value = String.lowercase_ascii (String.trim value)
let strip_keyword_prefix value =
if String.length value > 0 && value.[0] = ':' then
String.sub value 1 (String.length value - 1)
else value
let nonempty_trimmed = function
| Some value ->
let value = String.trim value in
if value = "" then None else Some value
| None -> None
let count_present values =
Vec.fold_left
(fun count -> function Some _ -> count + 1 | None -> count)
0 values
let invalid_uuid_option option_name = function
| Some value when not (Cli_primitive.is_uuid_string (String.trim value)) ->
Some ("Option " ^ option_name ^ " must be a valid UUID string")
| _ -> None
let first_error values = Vec.find_map Fun.id values
let invalid_uuid_options = function
| Parsed_block opts ->
first_error
(Vec.of_array
[|
invalid_uuid_option "uuid" opts.uuid;
invalid_uuid_option "target-uuid" opts.target_uuid;
|])
| Parsed_task opts ->
first_error
(Vec.of_array
[|
invalid_uuid_option "uuid" opts.uuid;
invalid_uuid_option "target-uuid" opts.target_uuid;
|])
| Parsed_asset opts ->
first_error
(Vec.of_array
[|
invalid_uuid_option "uuid" opts.uuid;
invalid_uuid_option "target-uuid" opts.target_uuid;
|])
| _ -> None
let update_opts_of_block_opts (opts : block_opts) : Update.opts =
{
id = opts.id;
uuid = opts.uuid;
target_id = opts.target_id;
target_uuid = opts.target_uuid;
target_page = opts.target_page;
pos = opts.pos;
status = None;
content = opts.content;
update_tags_edn = opts.update_tags_edn;
update_properties_edn = opts.update_properties_edn;
remove_tags_edn = opts.remove_tags_edn;
remove_properties_edn = opts.remove_properties_edn;
blocks_edn = opts.blocks_edn;
blocks_file = opts.blocks_file;
}
let invalid_options = function
| Parsed_block opts when update_mode opts ->
Update.invalid_options (update_opts_of_block_opts opts)
| Parsed_block opts
when (not (update_mode opts))
&& (Option.is_some opts.remove_tags_edn
|| Option.is_some opts.remove_properties_edn) ->
Some "--remove-tags and --remove-properties are only for update mode"
| Parsed_page opts
when count_present
(Vec.of_array
[|
Option.map Int64.to_string opts.id; nonempty_trimmed opts.page;
|])
> 1 ->
Some "only one of --id or --page is allowed"
| Parsed_page { page = Some page; _ } when String.trim page = "" ->
Some "page must be non-empty"
| Parsed_tag { name = Some name; _ } when normalize_tag_name name = None ->
Some "tag name must not be blank"
| Parsed_property opts
when count_present
(Vec.of_array
[|
Option.map Int64.to_string opts.id; nonempty_trimmed opts.name;
|])
> 1 ->
Some "only one of --id or --name is allowed"
| Parsed_property { name = Some name; _ } when String.trim name = "" ->
Some "name must be non-empty"
| Parsed_asset opts ->
let path = nonempty_trimmed opts.path in
let target_page = nonempty_trimmed opts.target_page in
let target_selectors =
Vec.of_array
[|
Option.map Int64.to_string opts.target_id;
opts.target_uuid;
target_page;
|]
in
let update_mode = Option.is_some opts.id || Option.is_some opts.uuid in
if Option.is_some opts.id && Option.is_some opts.uuid then
Some "only one of --id or --uuid is allowed"
else if update_mode && Option.is_some path then
Some "--path is only valid in create mode"
else if (not update_mode) && Option.is_none path then
Some "--path is required in create mode"
else if count_present target_selectors > 1 then
Some
"only one of --target-id, --target-uuid, or --target-page is allowed"
else if
update_mode
&& (count_present target_selectors > 0 || Option.is_some opts.pos)
then Some "--target-* and --pos are only valid in create mode"
else if Option.is_some opts.pos && count_present target_selectors = 0 then
Some "--pos is only valid when a target option is provided"
else if opts.pos = Some Block.Sibling && Option.is_some target_page then
Some "--pos sibling is only valid for block targets"
else None
| Parsed_task opts ->
let uuid = nonempty_trimmed opts.uuid in
let page = nonempty_trimmed opts.page in
let content = nonempty_trimmed opts.content in
let target_page = nonempty_trimmed opts.target_page in
let status = nonempty_trimmed opts.status in
let priority = nonempty_trimmed opts.priority in
let selectors =
Vec.of_array [| Option.map Int64.to_string opts.id; uuid; page |]
in
let target_selectors =
Vec.of_array
[|
Option.map Int64.to_string opts.target_id;
opts.target_uuid;
target_page;
|]
in
let selector_mode = count_present selectors > 0 in
if count_present selectors > 1 then
Some "only one of --id, --uuid, or --page is allowed"
else if Option.is_some page && Option.is_some content then
Some "--content and --page are mutually exclusive"
else if
(Option.is_some opts.id || Option.is_some uuid)
&& Option.is_some content
then Some "--content is only valid when creating a block task"
else if count_present target_selectors > 1 then
Some
"only one of --target-id, --target-uuid, or --target-page is allowed"
else if
selector_mode
&& (count_present target_selectors > 0 || Option.is_some opts.pos)
then
Some
"--target-* and --pos are only valid when creating a block task with \
--content"
else if Option.is_some opts.pos && count_present target_selectors = 0 then
Some "--pos is only valid when a target option is provided"
else if opts.pos = Some Block.Sibling && Option.is_some target_page then
Some "--pos sibling is only valid for block targets"
else if Option.is_some status && opts.no_status then
Some "--status and --no-status are mutually exclusive"
else if Option.is_some priority && opts.no_priority then
Some "--priority and --no-priority are mutually exclusive"
else if Option.is_some opts.scheduled && opts.no_scheduled then
Some "--scheduled and --no-scheduled are mutually exclusive"
else if Option.is_some opts.deadline && opts.no_deadline then
Some "--deadline and --no-deadline are mutually exclusive"
else None
| _ -> None
let command_id = function
| Parsed_block _ -> Command_id.Upsert_block
| Parsed_page _ -> Upsert_page
| Parsed_task _ -> Upsert_task
| Parsed_asset _ -> Upsert_asset
| Parsed_tag _ -> Upsert_tag
| Parsed_property _ -> Upsert_property
let validate_parsed parsed =
match invalid_uuid_options parsed with
| Some message -> Error (Error.invalid_options message)
| None -> (
match invalid_options parsed with
| Some message -> Error (Error.invalid_options message)
| None -> Ok ())
let repo_or_error config =
match config.Cli_config.repo with
| Some repo -> Ok (repo, Cli_config.repo_to_graph repo)
| None -> Error (Error.missing_repo "repo is required for upsert")
let ordinal value =
let suffix =
match value mod 100 with
| 11 | 12 | 13 -> "th"
| _ -> (
match value mod 10 with 1 -> "st" | 2 -> "nd" | 3 -> "rd" | _ -> "th")
in
string_of_int value ^ suffix
let default_journal_title () =
let year, month, day = Time.local_date (Time.now ()) in
let months =
[|
"Jan";
"Feb";
"Mar";
"Apr";
"May";
"Jun";
"Jul";
"Aug";
"Sep";
"Oct";
"Nov";
"Dec";
|]
in
months.(month - 1) ^ " " ^ ordinal day ^ ", " ^ string_of_int year
let block_target_of_parts target_id target_uuid target_page =
match
(target_id, nonempty_trimmed target_uuid, nonempty_trimmed target_page)
with
| Some id, None, None -> Ok (Target_id id)
| None, Some uuid, None -> Ok (Target_uuid uuid)
| None, None, Some page -> Ok (Target_page page)
| None, None, None ->
Error (Error.make Error.Missing_target "target page or block is required")
| _ ->
Error
(Error.invalid_options
"only one of --target-id, --target-uuid, or --target-page is allowed")
let optional_block_target_of_parts target_id target_uuid target_page =
match
(target_id, nonempty_trimmed target_uuid, nonempty_trimmed target_page)
with
| None, None, None -> Ok None
| Some id, None, None -> Ok (Some (Target_id id))
| None, Some uuid, None -> Ok (Some (Target_uuid uuid))
| None, None, Some page -> Ok (Some (Target_page page))
| _ ->
Error
(Error.invalid_options
"only one of --target-id, --target-uuid, or --target-page is allowed")
let add_action_of_block_create (action : block_create) : Add.action =
let target_id, target_uuid, target_page_name =
match action.target with
| Target_id id -> (Some id, None, None)
| Target_uuid uuid -> (None, Some uuid, None)
| Target_page page -> (None, None, Some page)
in
{
Add.repo = action.repo;
graph = action.graph;
target_id;
target_uuid;
target_page_name;
pos = action.pos;
status = action.status;
tags = action.tags;
properties = action.properties;
blocks = action.blocks;
}
let block_create_of_add_action ~update_plan (action : Add.action) =
Error.map
(fun target ->
{
repo = action.repo;
graph = action.graph;
target;
pos = action.pos;
status = action.status;
tags = action.tags;
properties = action.properties;
blocks = action.blocks;
update_plan;
})
(block_target_of_parts action.target_id action.target_uuid
action.target_page_name)
let block_update_of_update_action (action : Update.action) =
let source =
match (action.id, action.uuid) with
| Some id, None -> Ok (Source_id id)
| None, Some uuid -> Ok (Source_uuid uuid)
| _ -> Error (Error.make Error.Missing_source "source block is required")
in
Error.bind source (fun source ->
Error.map
(fun target ->
{
repo = action.repo;
graph = action.graph;
source;
target;
pos = action.pos;
update_tags = action.update_tags;
update_properties = action.update_properties;
remove_tags = action.remove_tags;
remove_properties = action.remove_properties;
content = action.content;
source_label = action.source_label;
target_label = action.target_label;
})
(optional_block_target_of_parts action.target_id action.target_uuid
action.target_page))
let update_action_of_block_update (action : block_update) : Update.action =
let id, uuid =
match action.source with
| Source_id id -> (Some id, None)
| Source_uuid uuid -> (None, Some uuid)
in
let target_id, target_uuid, target_page =
match action.target with
| None -> (None, None, None)
| Some (Target_id id) -> (Some id, None, None)
| Some (Target_uuid uuid) -> (None, Some uuid, None)
| Some (Target_page page) -> (None, None, Some page)
in
{
Update.repo = action.repo;
graph = action.graph;
id;
uuid;
target_id;
target_uuid;
target_page;
pos = action.pos;
update_tags = action.update_tags;
update_properties = action.update_properties;
remove_tags = action.remove_tags;
remove_properties = action.remove_properties;
content = action.content;
source_label = action.source_label;
target_label = action.target_label;
}
let normalize_page_name_input value =
let value = String.trim value in
if value = "" then None else Some value
let strip_tag_prefix value =
let value = String.trim value in
let rec loop idx =
if idx < String.length value && value.[idx] = '#' then loop (idx + 1)
else idx
in
let start = loop 0 in
String.sub value start (String.length value - start) |> String.trim
let tag_of_value value =
match
( Edn_util.as_int64 value,
Edn_util.as_uuid value,
Edn_util.as_keyword_t value,
Edn_util.as_string value )
with
| Some id, _, _, _ -> Some (Selector.Tag_id id)
| _, Some uuid, _, _ -> Some (Tag_uuid uuid)
| _, _, Some ident, _ -> Some (Tag_ident ident)
| _, _, _, Some value ->
let value = strip_tag_prefix value in
if value = "" then None
else if Cli_primitive.is_uuid_string value then Some (Tag_uuid value)
else if value.[0] = ':' then Some (Tag_ident (Edn_util.keyword_t value))
else Some (Tag_name value)
| _ -> None
let key_of_value = Property.parse_key
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 parse_vector_edn ~label text =
Error.bind (edn_value_of_string ~label text) (fun parsed ->
match Edn_util.as_vector parsed with
| Some values -> Ok values
| _ -> Error (Error.invalid_options (label ^ " must be a vector")))
let parse_tags_edn ~label = function
| None -> Ok Vec.empty
| Some text ->
Error.bind (parse_vector_edn ~label text) (fun values ->
let rec loop acc remaining =
match Vec.pop_front remaining with
| None -> Ok acc
| Some (value, rest) -> (
match tag_of_value value with
| Some tag -> loop (Vec.push_back acc tag) rest
| None ->
Error
(Error.invalid_options
(label
^ " must contain strings, keywords, uuids, or ids")))
in
loop Vec.empty values)
let parse_property_keys_edn ~label = function
| None -> Ok Vec.empty
| Some text ->
Error.bind (parse_vector_edn ~label text) (fun values ->
let rec loop acc remaining =
match Vec.pop_front remaining with
| None -> Ok acc
| Some (value, rest) -> (
match key_of_value value with
| Some key -> loop (Vec.push_back acc key) rest
| None ->
Error
(Error.invalid_options
(label ^ " must contain property keys")))
in
loop Vec.empty values)
let parse_tag_schema_properties opts =
Error.bind
(parse_property_keys_edn ~label:"add-properties" opts.add_properties_edn)
(fun add_properties ->
Error.map
(fun remove_properties -> (add_properties, remove_properties))
(parse_property_keys_edn ~label:"remove-properties"
opts.remove_properties_edn))
let build_tag repo graph (opts : tag_opts) =
Error.bind (parse_tag_schema_properties opts)
(fun (add_properties, remove_properties) ->
match (opts.id, Option.bind opts.name normalize_tag_name) with
| Some id, name ->
Ok
(Upsert_tag
{
repo;
graph;
mode = Update;
id = Some id;
name;
add_properties;
remove_properties;
})
| None, Some name ->
Ok
(Upsert_tag
{
repo;
graph;
mode = Create;
id = None;
name = Some name;
add_properties;
remove_properties;
})
| None, None ->
Error (Error.make Error.Missing_tag_name "tag name is required"))
let parse_property_assignments_edn ~label = function
| None -> Ok Vec.empty
| Some text ->
Error.bind (edn_value_of_string ~label text) (fun parsed ->
match Edn_util.as_map parsed with
| Some fields ->
let rec loop acc remaining =
match Vec.pop_front remaining with
| None -> Ok acc
| Some ((key, value), rest) -> (
match key_of_value key with
| Some key ->
loop (Vec.push_back acc { Property.key; value }) rest
| None ->
Error
(Error.invalid_options
(label ^ " must contain property keys")))
in
loop Vec.empty fields
| _ -> Error (Error.invalid_options (label ^ " must be a map")))
let parse_update_plan (opts : page_opts) =
Error.bind (parse_tags_edn ~label:"update-tags" opts.update_tags_edn)
(fun update_tags ->
Error.bind (parse_tags_edn ~label:"remove-tags" opts.remove_tags_edn)
(fun remove_tags ->
Error.bind
(parse_property_assignments_edn ~label:"update-properties"
opts.update_properties_edn) (fun update_properties ->
Error.bind
(parse_property_keys_edn ~label:"remove-properties"
opts.remove_properties_edn) (fun remove_properties ->
Ok
{
Property.update_tags;
remove_tags;
update_properties;
remove_properties;
}))))
let build_page repo graph (opts : page_opts) =
Error.bind (parse_update_plan opts) (fun plan ->
match (opts.id, Option.bind opts.page normalize_page_name_input) with
| Some id, page ->
Ok
(Upsert_page
{
repo;
graph;
mode = Update;
id = Some id;
page;
restore = opts.restore;
plan;
})
| None, Some page ->
Ok
(Upsert_page
{
repo;
graph;
mode = Create;
id = None;
page = Some page;
restore = opts.restore;
plan;
})
| None, None ->
Error (Error.make Error.Missing_page_name "page name is required"))
let schema_of_property_opts (opts : property_opts) =
{
Property.kind = opts.kind;
cardinality = opts.cardinality;
hidden = opts.hide;
public = opts.public;
}
let normalize_property_name value =
let value = String.trim value in
if value = "" then None else Some value
let build_property repo graph (opts : property_opts) =
let schema = schema_of_property_opts opts in
match (opts.id, Option.bind opts.name normalize_property_name) with
| Some id, name ->
Ok
(Upsert_property
{ repo; graph; mode = Update; id = Some id; name; schema })
| None, Some name ->
Ok
(Upsert_property
{ repo; graph; mode = Create; id = None; name = Some name; schema })
| None, None ->
Error (Error.make Error.Missing_property_name "property name is required")
let normalize_content value =
let value = String.trim value in
if value = "" then None else Some value
let normalize_priority value =
match String.lowercase_ascii (String.trim value) with
| "low" -> Some (Edn_util.keyword_t "logseq.property/priority.low")
| "medium" -> Some (Edn_util.keyword_t "logseq.property/priority.medium")
| "high" -> Some (Edn_util.keyword_t "logseq.property/priority.high")
| "urgent" -> Some (Edn_util.keyword_t "logseq.property/priority.urgent")
| _ -> None
let invalid_priority_message value =
"Invalid value for option :priority: " ^ value
^ ". Available values: low, medium, high, urgent"
let build_asset repo graph (opts : asset_opts) =
let content = Option.bind opts.content normalize_content in
match (opts.id, opts.uuid, opts.path) with
| Some id, None, _ ->
Ok
(Upsert_asset
{
repo;
graph;
mode = Update;
id = Some id;
uuid = None;
path = None;
content;
create_action = None;
})
| None, Some uuid, _ ->
Ok
(Upsert_asset
{
repo;
graph;
mode = Update;
id = None;
uuid = Some uuid;
path = None;
content;
create_action = None;
})
| None, None, Some path ->
let title =
match content with
| Some content -> content
| None -> Filename.basename path
in
let add_opts =
{
Add.target_id = opts.target_id;
target_uuid = opts.target_uuid;
target_page_name = opts.target_page;
pos = opts.pos;
status = None;
tags_edn = None;
properties_edn = None;
content = Some title;
blocks_edn = None;
blocks_file = None;
}
in
Error.bind (Add.build_add_block_action add_opts Vec.empty repo)
(fun create_action ->
Error.map
(fun create_action ->
Upsert_asset
{
repo;
graph;
mode = Create;
id = None;
uuid = None;
path = Some path;
content = Some title;
create_action = Some create_action;
})
(block_create_of_add_action ~update_plan:Property.empty_update_plan
create_action))
| None, None, None ->
Error
(Error.make Error.Missing_asset_selector
"asset id, uuid, or path is required")
| Some _, Some _, _ ->
Error (Error.invalid_options "only one of --id or --uuid is allowed")
let build_task repo graph (opts : task_opts) =
let priority = Option.bind opts.priority normalize_priority in
match (opts.priority, priority) with
| Some value, None ->
Error (Error.invalid_options (invalid_priority_message value))
| _ -> (
let clear_properties =
let properties = Vec.empty in
let properties =
if opts.no_status then
Vec.push_back properties
(Property.Key_ident (Edn_util.keyword_t "logseq.property/status"))
else properties
in
let properties =
if opts.no_priority then
Vec.push_back properties
(Property.Key_ident
(Edn_util.keyword_t "logseq.property/priority"))
else properties
in
let properties =
if opts.no_scheduled then
Vec.push_back properties
(Property.Key_ident
(Edn_util.keyword_t "logseq.property/scheduled"))
else properties
in
let properties =
if opts.no_deadline then
Vec.push_back properties
(Property.Key_ident
(Edn_util.keyword_t "logseq.property/deadline"))
else properties
in
properties
in
let update_properties =
let properties = Vec.empty in
let properties =
match priority with
| Some priority ->
Vec.push_back properties
{
Property.key =
Property.Key_ident
(Edn_util.keyword_t "logseq.property/priority");
value = Edn_util.any priority;
}
| None -> properties
in
let properties =
match opts.scheduled with
| Some scheduled ->
Vec.push_back properties
{
Property.key =
Property.Key_ident
(Edn_util.keyword_t "logseq.property/scheduled");
value = Edn_util.float (Js.Date.getTime scheduled);
}
| None -> properties
in
let properties =
match opts.deadline with
| Some deadline ->
Vec.push_back properties
{
Property.key =
Property.Key_ident
(Edn_util.keyword_t "logseq.property/deadline");
value = Edn_util.float (Js.Date.getTime deadline);
}
| None -> properties
in
properties
in
match (opts.page, opts.id, opts.uuid, opts.content) with
| Some page, None, None, _ when String.trim page <> "" ->
Ok
(Upsert_task
{
repo;
graph;
mode = Page;
id = None;
uuid = None;
page = Some page;
content = None;
update_properties;
clear_properties;
status_input = opts.status;
})
| None, Some id, None, _ ->
Ok
(Upsert_task
{
repo;
graph;
mode = Update;
id = Some id;
uuid = None;
page = None;
content = opts.content;
update_properties;
clear_properties;
status_input = opts.status;
})
| None, None, Some uuid, _ ->
Ok
(Upsert_task
{
repo;
graph;
mode = Update;
id = None;
uuid = Some uuid;
page = None;
content = opts.content;
update_properties;
clear_properties;
status_input = opts.status;
})
| None, None, None, Some content when String.trim content <> "" -> (
match
(opts.target_id, opts.target_uuid, opts.target_page, opts.pos)
with
| Some _, _, _, _ | _, Some _, _, _ | _, _, _, Some _ ->
Error
(Error.make Error.Spec_blocker
"upsert task create currently supports only --target-page \
because spec/l3/upsert.mli does not carry target id, \
target uuid, or pos in Upsert_task actions")
| None, None, Some target_page, None
when String.trim target_page <> "" ->
Ok
(Upsert_task
{
repo;
graph;
mode = Create;
id = None;
uuid = None;
page = Some target_page;
content = Some content;
update_properties;
clear_properties;
status_input = opts.status;
})
| None, None, None, None ->
Ok
(Upsert_task
{
repo;
graph;
mode = Create;
id = None;
uuid = None;
page = Some (default_journal_title ());
content = Some content;
update_properties;
clear_properties;
status_input = opts.status;
})
| _ ->
Error
(Error.make Error.Missing_target
"target page is required when creating a task block"))
| _ -> Error (Error.make Error.Missing_target "block or page is required")
)
let parse_block_update_plan (opts : block_opts) =
Error.bind (parse_tags_edn ~label:"update-tags" opts.update_tags_edn)
(fun update_tags ->
Error.bind (parse_tags_edn ~label:"remove-tags" opts.remove_tags_edn)
(fun remove_tags ->
Error.bind
(parse_property_assignments_edn ~label:"update-properties"
opts.update_properties_edn) (fun update_properties ->
Error.bind
(parse_property_keys_edn ~label:"remove-properties"
opts.remove_properties_edn) (fun remove_properties ->
Ok
{
Property.update_tags;
remove_tags;
update_properties;
remove_properties;
}))))
let build_block repo (opts : block_opts) =
Error.bind (parse_block_update_plan opts) (fun plan ->
if update_mode opts then
Error.bind
(Update.build_action (update_opts_of_block_opts opts) repo)
(fun action ->
Error.map
(fun action -> Upsert_block (Block_update action))
(block_update_of_update_action action))
else
let target_page =
match
( opts.target_id,
nonempty_trimmed opts.target_uuid,
nonempty_trimmed opts.target_page )
with
| None, None, None -> Some (default_journal_title ())
| _ -> opts.target_page
in
let add_opts =
{
Add.target_id = opts.target_id;
target_uuid = opts.target_uuid;
target_page_name = target_page;
pos = opts.pos;
status = None;
tags_edn = None;
properties_edn = None;
content = opts.content;
blocks_edn = opts.blocks_edn;
blocks_file = opts.blocks_file;
}
in
Error.bind (Add.build_add_block_action add_opts Vec.empty repo)
(fun action ->
Error.map
(fun action -> Upsert_block (Block_create action))
(block_create_of_add_action ~update_plan:plan action)))
let build ?registry:_ config _globals parsed =
Error.bind (validate_parsed parsed) (fun () ->
Error.bind (repo_or_error config) (fun (repo, graph) ->
match parsed with
| Parsed_block opts -> build_block repo opts
| Parsed_page opts -> build_page repo graph opts
| Parsed_tag opts -> build_tag repo graph opts
| Parsed_property opts -> build_property repo graph opts
| Parsed_asset opts -> build_asset repo graph opts
| Parsed_task opts -> build_task repo graph opts))
let kw value = Edn_util.keyword value
let sym value = Edn_util.symbol value
let vector_vec values = Edn_util.vector_vec values
let list_vec values = Edn_util.list_vec values
let query_value query =
Edn_util.any (Cli_primitive.datascript_query_to_edn query)
let tag_selector =
vector_vec
(Vec.of_array
[|
kw "db/id";
kw "block/uuid";
kw "block/name";
kw "block/title";
Edn_util.map_vec
(Vec.of_array
[|
(kw "block/tags", vector_vec (Vec.of_array [| kw "db/ident" |]));
|]);
|])
let page_selector =
vector_vec
(Vec.of_array
[|
kw "db/id";
kw "block/uuid";
kw "block/name";
kw "block/title";
kw "logseq.property/deleted-at";
|])
let property_selector =
vector_vec
(Vec.of_array
[|
kw "db/id";
kw "db/ident";
kw "block/uuid";
kw "block/name";
kw "block/title";
kw "logseq.property/type";
|])
let asset_selector =
vector_vec
(Vec.of_array
[|
kw "db/id";
kw "block/uuid";
kw "block/title";
kw "logseq.property/deleted-at";
Edn_util.map_vec
(Vec.of_array
[|
(kw "block/tags", vector_vec (Vec.of_array [| kw "db/ident" |]));
|]);
|])
let task_selector =
vector_vec
(Vec.of_array
[| kw "db/id"; kw "block/uuid"; kw "block/name"; kw "block/title" |])
let result_ids ids =
Edn_util.map_vec
(Vec.of_array
[|
( kw "result",
Edn_util.vector_vec (ids |> Vec.map (fun id -> Edn_util.int64 id)) );
|])
module Node_crypto = struct
type hash
external create_hash : string -> hash = "createHash" [@@mel.module "crypto"]
external update_buffer : hash -> Node.Buffer.t -> hash = "update" [@@mel.send]
external digest : hash -> string -> string = "digest" [@@mel.send]
end
let file_sha256 path =
try
let payload = Cli_unix.read_binary_file path in
let hash = Node_crypto.create_hash "sha256" in
let payload_buffer =
Node.Buffer.fromStringWithEncoding payload ~encoding:`latin1
in
Some
(Node_crypto.digest (Node_crypto.update_buffer hash payload_buffer) "hex")
with _ -> None
let file_size path = (Cli_unix.stat path).Cli_unix.st_size
let file_extension path =
let base = Filename.basename path in
match String.rindex_opt base '.' with
| Some index when index + 1 < String.length base ->
Some
(String.sub base (index + 1) (String.length base - index - 1)
|> String.lowercase_ascii)
| _ -> None
let rec mkdir_p path =
if path = "" || path = Filename.dirname path || Cli_unix.file_exists path then
()
else (
mkdir_p (Filename.dirname path);
Cli_unix.mkdir path 0o755)
let copy_file source destination =
mkdir_p (Filename.dirname destination);
Cli_unix.copy_file source destination
let graph_assets_dir config repo =
Filename.concat
(Filename.concat
(Filename.concat config.Cli_config.root_dir "graphs")
(Graph_dir.graph_dir_name_of_repo repo))
"assets"
let class_query selector class_ident =
Cli_primitive.make_datascript_query
~find:
(Vec.singleton
(vector_vec
(Vec.of_array
[|
list_vec (Vec.of_array [| sym "pull"; sym "?e"; selector |]);
sym "...";
|])))
~in_:
(Vec.of_array
[|
Melange_edn_melange.symbol "$"; Melange_edn_melange.symbol "?name";
|])
~where:
(Vec.of_array
[|
Cli_primitive.V
(Edn_util.vector_t_vec
(Vec.of_array [| sym "?e"; kw "block/name"; sym "?name" |]));
Cli_primitive.V
(Edn_util.vector_t_vec
(Vec.of_array [| sym "?e"; kw "block/tags"; sym "?t" |]));
Cli_primitive.V
(Edn_util.vector_t_vec
(Vec.of_array [| sym "?t"; kw "db/ident"; kw class_ident |]));
|])
()
let tag_query selector = class_query selector "logseq.class/Tag"
let property_query selector = class_query selector "logseq.class/Property"
let page_query selector =
Cli_primitive.make_datascript_query
~find:
(Vec.singleton
(vector_vec
(Vec.of_array
[|
list_vec (Vec.of_array [| sym "pull"; sym "?e"; selector |]);
sym "...";
|])))
~in_:
(Vec.of_array
[|
Melange_edn_melange.symbol "$"; Melange_edn_melange.symbol "?name";
|])
~where:
(Vec.of_array
[|
Cli_primitive.V
(Edn_util.vector_t_vec
(Vec.of_array [| sym "?e"; kw "block/name"; sym "?name" |]));
|])
()
let pull_pages_by_name config repo name selector =
Transport.thread_api_q config ~repo
~query:
(Edn_util.vector_t_vec
(Vec.of_array
[|
query_value (page_query selector);
Edn_util.string (normalized_lookup_name name);
|]))
let pull_tag_by_name config repo name selector =
Transport.thread_api_q config ~repo
~query:
(Edn_util.vector_t_vec
(Vec.of_array
[|
query_value (tag_query selector);
Edn_util.string (normalized_lookup_name name);
|]))
let list_tags config repo =
Transport.thread_api_cli_list_tags config ~repo
~options:(Edn_util.map_t_vec Vec.empty)
let tag_name_matches name entity =
let expected = normalized_lookup_name name in
let matches value = String.equal (normalized_lookup_name value) expected in
match
( Edn_util.get_string entity "block/title",
Edn_util.get_string entity "block/name" )
with
| Some title, _ when matches title -> true
| _, Some name when matches name -> true
| _ -> false
let find_tag_by_name config repo name =
let open Cli_effect in
bind (list_tags config repo) (fun value ->
match Edn_util.as_seq value with
| Some tags -> pure (Vec.find_opt (tag_name_matches name) tags)
| None -> pure None)
let pull_property_by_name config repo name selector =
Transport.thread_api_q config ~repo
~query:
(Edn_util.vector_t_vec
(Vec.of_array
[|
query_value (property_query selector);
Edn_util.string (normalized_lookup_name name);
|]))
let pull_entity_by_id config repo selector id =
Transport.thread_api_pull config ~repo
~selector:(Edn_util.expect_vector_t "upsert pull selector" selector)
~lookup:(Edn_util.int64 id)
let pull_entity_by_lookup config repo selector lookup =
Transport.thread_api_pull config ~repo
~selector:(Edn_util.expect_vector_t "upsert pull selector" selector)
~lookup
let first_entity value =
match (Edn_util.as_vector value, Edn_util.as_list value) with
| Some values, _ when not (Vec.is_empty values) ->
Some (Vec.peek_front values)
| _, Some values when not (Vec.is_empty values) ->
Some (Vec.peek_front values)
| _ -> None
let id_of_entity value = Edn_util.get_int64 value "db/id"
let uuid_of_entity value =
Option.bind (Edn_util.get value "block/uuid") Edn_util.as_string_like
let name_of_entity value = Edn_util.get_string value "block/name"
let tag_entity value =
match Option.bind (Edn_util.get value "block/tags") Edn_util.as_seq with
| Some tags ->
Vec.exists
(fun tag ->
match
Option.bind (Edn_util.get tag "db/ident") Edn_util.as_string_like
with
| Some ident -> strip_keyword_prefix ident = "logseq.class/Tag"
| _ -> false)
tags
| _ -> false
let property_entity value =
Option.is_some (Edn_util.get value "logseq.property/type")
let asset_entity value =
match Option.bind (Edn_util.get value "block/tags") Edn_util.as_seq with
| Some tags ->
Vec.exists
(fun tag ->
match
Option.bind (Edn_util.get tag "db/ident") Edn_util.as_string_like
with
| Some ident -> strip_keyword_prefix ident = "logseq.class/Asset"
| _ -> false)
tags
| _ -> false
let ident_of_entity value =
Option.bind (Edn_util.get value "db/ident") Edn_util.as_keyword_t
let lookup_of_tag = function
| Selector.Tag_id id -> Edn_util.int64 id
| Tag_name name -> Edn_util.string (normalized_lookup_name name)
| Tag_ident ident -> Edn_util.any ident
| Tag_uuid uuid -> Edn_util.uuid uuid
let apply_outliner_ops config repo ops =
Transport.thread_api_apply_outliner_ops config ~repo
~ops:(Edn_util.vector_t_vec ops)
~options:(Edn_util.map_t_vec Vec.empty)
let create_tag_page config repo name =
let op =
Edn_util.vector_vec
(Vec.of_array
[|
kw "create-page";
Edn_util.vector_vec
(Vec.of_array
[|
Edn_util.string name;
Edn_util.map_vec
(Vec.of_array [| (kw "class?", Edn_util.bool true) |]);
|]);
|])
in
apply_outliner_ops config repo (Vec.singleton op)
let create_page config repo name =
let op =
Edn_util.vector_vec
(Vec.of_array
[|
kw "create-page";
Edn_util.vector_vec
(Vec.of_array
[| Edn_util.string name; Edn_util.map_vec Vec.empty |]);
|])
in
apply_outliner_ops config repo (Vec.singleton op)
let rename_page config repo uuid name =
let op =
Edn_util.vector_vec
(Vec.of_array
[|
kw "rename-page";
Edn_util.vector_vec
(Vec.of_array [| Edn_util.uuid uuid; Edn_util.string name |]);
|])
in
apply_outliner_ops config repo (Vec.singleton op)
let value_of_kind kind = Edn_util.keyword (Property.string_of_kind kind)
let value_of_cardinality = function
| Property.One -> Edn_util.keyword "db.cardinality/one"
| Many -> Edn_util.keyword "db.cardinality/many"
let value_of_schema (schema : Property.schema) =
let fields = Vec.empty in
let fields =
match schema.kind with
| Some kind ->
Vec.push_back fields (kw "logseq.property/type", value_of_kind kind)
| None -> fields
in
let fields =
match schema.cardinality with
| Some cardinality ->
Vec.push_back fields
(kw "db/cardinality", value_of_cardinality cardinality)
| None -> fields
in
let fields =
match schema.hidden with
| Some hidden ->
Vec.push_back fields (kw "logseq.property/hide?", Edn_util.bool hidden)
| None -> fields
in
let fields =
match schema.public with
| Some public ->
Vec.push_back fields (kw "logseq.property/public?", Edn_util.bool public)
| None -> fields
in
Edn_util.map_vec fields
let schema_empty (schema : Property.schema) =
schema.kind = None && schema.cardinality = None && schema.hidden = None
&& schema.public = None
let upsert_property_op ?(opts = Edn_util.map_vec Vec.empty) ident schema =
let ident =
match ident with Some ident -> Edn_util.any ident | None -> Edn_util.nil
in
Edn_util.vector_vec
(Vec.of_array
[|
kw "upsert-property";
Edn_util.vector_vec
(Vec.of_array [| ident; value_of_schema schema; opts |]);
|])
let upsert_property config repo ?opts ident schema =
apply_outliner_ops config repo
(Vec.singleton (upsert_property_op ?opts ident schema))
let ok_tag_ids mode ids =
Cli_result.ok ~command:Command_id.Upsert_tag mode (Raw (result_ids ids))
let ok_page_ids mode ids =
Cli_result.ok ~command:Command_id.Upsert_page mode (Raw (result_ids ids))
let ok_property_ids mode ids =
Cli_result.ok ~command:Command_id.Upsert_property mode (Raw (result_ids ids))
let ok_asset_ids mode ids =
Cli_result.ok ~command:Command_id.Upsert_asset mode (Raw (result_ids ids))
let tag_not_found mode =
Cli_result.error ~command:Command_id.Upsert_tag mode
(Error.make Error.Tag_not_found "tag not found after upsert")
let upsert_id_not_found entity_type id =
Error.make Error.Upsert_id_not_found
(entity_type ^ " not found for id")
~context:
(Edn_util.map_vec
(Vec.of_array
[|
(kw "entity-type", Edn_util.string entity_type);
(kw "id", Edn_util.int64 id);
|]))
let upsert_id_type_mismatch entity_type id =
Error.make Error.Upsert_id_type_mismatch
("id must be a node tagged with #" ^ entity_type)
~context:
(Edn_util.map_vec
(Vec.of_array
[|
(kw "entity-type", Edn_util.string entity_type);
(kw "id", Edn_util.int64 id);
|]))
let same_tag_name entity target_name =
match name_of_entity entity with
| Some current_name ->
String.equal current_name (normalized_lookup_name target_name)
| None -> false
let rename_conflict current_entity target =
match (id_of_entity target, id_of_entity current_entity) with
| None, _ -> None
| Some target_id, Some current_id when target_id = current_id -> None
| Some _, _ when tag_entity target ->
Some
(Error.make Error.Tag_rename_conflict
"rename target already exists as a tag")
| Some _, _ ->
Some
(Error.make Error.Tag_name_conflict
"tag already exists as a page and is not a tag")
let page_entity value = Option.is_some (name_of_entity value)
let recycled_entity value =
Option.is_some (Edn_util.get value "logseq.property/deleted-at")
let page_not_found () = Error.make Error.Page_not_found "page not found"
let recycled_page_error () = Error.make Error.Recycled_page "page is recycled"
let resolve_tag_id invoke_config repo tag =
let open Cli_effect in
match tag with
| Selector.Tag_id id -> pure (Ok id)
| Tag_name name ->
bind (find_tag_by_name invoke_config repo name) (fun result ->
match Option.bind result id_of_entity with
| Some id -> pure (Ok id)
| None ->
pure (Error (Error.make Error.Tag_not_found "tag not found")))
| Tag_ident _ | Tag_uuid _ ->
bind
(pull_entity_by_lookup invoke_config repo tag_selector
(lookup_of_tag tag))
(fun result ->
match id_of_entity result with
| Some id -> pure (Ok id)
| None ->
pure (Error (Error.make Error.Tag_not_found "tag not found")))
let property_key_label = function
| Property.Key_ident ident ->
Edn_util.keyword_to_string ident |> strip_keyword_prefix
| Key_id id -> Int64.to_string id
| Key_name name -> name
let string_starts_with ~prefix value =
let prefix_len = String.length prefix in
String.length value >= prefix_len && String.sub value 0 prefix_len = prefix
let qualified_property_ident ident =
String.contains (property_key_label (Property.Key_ident ident)) '/'
let property_not_found_error key =
let label = property_key_label key in
Error.make Error.Property_not_found ("property not found: " ^ label)
let resolved_property_ident key entity =
match (id_of_entity entity, ident_of_entity entity) with
| Some _, Some ident -> Ok ident
| _ -> Error (property_not_found_error key)
let resolved_property_ident_from_query key result =
match first_entity result with
| Some entity -> resolved_property_ident key entity
| None -> Error (property_not_found_error key)
let resolved_schema_property_ident key entity =
if property_entity entity then resolved_property_ident key entity
else Error (property_not_found_error key)
let resolved_schema_property_ident_from_query key result =
match first_entity result with
| Some entity -> resolved_schema_property_ident key entity
| None -> Error (property_not_found_error key)
let resolve_property_ident invoke_config repo key =
let open Cli_effect in
match key with
| Property.Key_ident ident when qualified_property_ident ident ->
pure (Ok ident)
| Property.Key_ident ident ->
bind
(pull_entity_by_lookup invoke_config repo
(vector_vec (Vec.of_array [| kw "db/id" |]))
(vector_vec (Vec.of_array [| kw "db/ident"; Edn_util.any ident |])))
(fun result ->
match resolved_property_ident key result with
| Ok ident -> pure (Ok ident)
| Error _ when not (qualified_property_ident ident) ->
bind
(pull_property_by_name invoke_config repo
(property_key_label key) property_selector)
(fun result ->
pure (resolved_property_ident_from_query key result))
| Error err -> pure (Error err))
| Key_id id ->
bind
(pull_entity_by_lookup invoke_config repo
(vector_vec (Vec.of_array [| kw "db/ident" |]))
(Edn_util.int64 id))
(fun result ->
match ident_of_entity result with
| Some ident -> pure (Ok ident)
| None -> pure (Error (property_not_found_error key)))
| Key_name name ->
bind (pull_property_by_name invoke_config repo name property_selector)
(fun result ->
match resolved_property_ident_from_query key result with
| Ok ident -> pure (Ok ident)
| Error _ ->
let ident = Edn_util.keyword_t name in
bind
(pull_entity_by_lookup invoke_config repo
(vector_vec (Vec.of_array [| kw "db/id" |]))
(vector_vec
(Vec.of_array [| kw "db/ident"; Edn_util.any ident |])))
(fun result ->
if Option.is_some (id_of_entity result) then pure (Ok ident)
else pure (Error (property_not_found_error key))))
let resolve_schema_property_ident invoke_config repo key =
let open Cli_effect in
match key with
| Property.Key_ident ident ->
bind
(pull_entity_by_lookup invoke_config repo property_selector
(vector_vec (Vec.of_array [| kw "db/ident"; Edn_util.any ident |])))
(fun result ->
match resolved_schema_property_ident key result with
| Ok ident -> pure (Ok ident)
| Error _ when not (qualified_property_ident ident) ->
bind
(pull_property_by_name invoke_config repo
(property_key_label key) property_selector)
(fun result ->
pure (resolved_schema_property_ident_from_query key result))
| Error err -> pure (Error err))
| Key_id id ->
bind
(pull_entity_by_lookup invoke_config repo property_selector
(Edn_util.int64 id))
(fun result -> pure (resolved_schema_property_ident key result))
| Key_name name ->
bind (pull_property_by_name invoke_config repo name property_selector)
(fun result ->
match resolved_schema_property_ident_from_query key result with
| Ok ident -> pure (Ok ident)
| Error _ ->
let ident = Edn_util.keyword_t name in
bind
(pull_entity_by_lookup invoke_config repo property_selector
(vector_vec
(Vec.of_array [| kw "db/ident"; Edn_util.any ident |])))
(fun result -> pure (resolved_schema_property_ident key result)))
let resolve_tag_ids invoke_config repo tags =
let open Cli_effect in
let rec loop acc remaining =
match Vec.pop_front remaining with
| None -> pure (Ok acc)
| Some (tag, rest) ->
bind (resolve_tag_id invoke_config repo tag) (function
| Error err -> pure (Error err)
| Ok id -> loop (Vec.push_back acc id) rest)
in
loop Vec.empty tags
let resolve_property_keys invoke_config repo keys =
let open Cli_effect in
let rec loop acc remaining =
match Vec.pop_front remaining with
| None -> pure (Ok acc)
| Some (key, rest) ->
bind (resolve_property_ident invoke_config repo key) (function
| Error err -> pure (Error err)
| Ok ident -> loop (Vec.push_back acc ident) rest)
in
loop Vec.empty keys
let resolve_schema_property_keys invoke_config repo keys =
let open Cli_effect in
let rec loop acc remaining =
match Vec.pop_front remaining with
| None -> pure (Ok acc)
| Some (key, rest) ->
bind (resolve_schema_property_ident invoke_config repo key) (function
| Error err -> pure (Error err)
| Ok ident -> loop (Vec.push_back acc ident) rest)
in
loop Vec.empty keys
let block_uuid_lookup_ref value =
match (Edn_util.as_vector value, Edn_util.as_list value) with
| Some values, _ when Vec.length values = 2 -> (
match
( Edn_util.as_string_like (Vec.nth values 0),
Edn_util.as_string_like (Vec.nth values 1) )
with
| Some "block/uuid", Some uuid -> Some uuid
| _ -> None)
| _, Some values when Vec.length values = 2 -> (
match
( Edn_util.as_string_like (Vec.nth values 0),
Edn_util.as_string_like (Vec.nth values 1) )
with
| Some "block/uuid", Some uuid -> Some uuid
| _ -> None)
| _ -> None
let resolve_block_uuid_ref invoke_config repo uuid =
let open Cli_effect in
bind
(pull_entity_by_lookup invoke_config repo
(vector_vec (Vec.of_array [| kw "db/id"; kw "block/uuid" |]))
(vector_vec (Vec.of_array [| kw "block/uuid"; Edn_util.uuid uuid |])))
(fun entity ->
match id_of_entity entity with
| Some id -> pure (Ok (Edn_util.int64 id))
| None ->
pure
(Error
(Error.make Error.Block_not_found ("block not found: " ^ uuid))))
let rec resolve_property_value_refs invoke_config repo value =
let open Cli_effect in
match block_uuid_lookup_ref value with
| Some uuid -> resolve_block_uuid_ref invoke_config repo uuid
| None -> (
let resolve_values wrap values =
let rec loop acc remaining =
match Vec.pop_front remaining with
| None -> pure (Ok (wrap acc))
| Some (value, rest) ->
bind (resolve_property_value_refs invoke_config repo value)
(function
| Error err -> pure (Error err)
| Ok value -> loop (Vec.push_back acc value) rest)
in
loop Vec.empty values
in
match value with
| Melange_edn_melange.Any (Melange_edn_melange.Vector values) ->
resolve_values Edn_util.vector_vec (Edn_util.vec_of_array values)
| Any (List values) ->
resolve_values Edn_util.list_vec (Edn_util.vec_of_array values)
| Any (Set values) ->
resolve_values Edn_util.set_vec (Edn_util.vec_of_array values)
| Any (Map fields) ->
let rec loop acc remaining =
match Vec.pop_front remaining with
| None -> pure (Ok (Edn_util.map_vec acc))
| Some ((key, value), rest) ->
bind (resolve_property_value_refs invoke_config repo value)
(function
| Error err -> pure (Error err)
| Ok value -> loop (Vec.push_back acc (key, value)) rest)
in
loop Vec.empty (Edn_util.vec_of_array fields)
| _ -> pure (Ok value))
let resolve_property_assignments invoke_config repo assignments =
let open Cli_effect in
let rec loop acc remaining =
match Vec.pop_front remaining with
| None -> pure (Ok acc)
| Some (assignment, rest) ->
bind (resolve_property_ident invoke_config repo assignment.Property.key)
(function
| Error err -> pure (Error err)
| Ok ident ->
bind
(resolve_property_value_refs invoke_config repo assignment.value)
(function
| Error err -> pure (Error err)
| Ok value -> loop (Vec.push_back acc (ident, value)) rest))
in
loop Vec.empty assignments
let option_resolution_error option err =
{
err with
Error.context =
Some
(Edn_util.map_vec
(Vec.of_array
[|
(kw "option", Edn_util.string option);
(kw "phase", Edn_util.string "resolve-options");
|]));
}
let structural_block_field key =
match Edn_util.as_string_like key with
| Some key ->
key = "block/title" || key = "block/content" || key = "block/uuid"
|| key = "block/children" || key = "block/tags"
|| string_starts_with ~prefix:"db/" key
|| string_starts_with ~prefix:"build/" key
| None -> false
let inline_property_assignments block =
match Edn_util.as_map block.Block.raw with
| None -> Ok Vec.empty
| Some fields ->
let rec loop acc remaining =
match Vec.pop_front remaining with
| None -> Ok acc
| Some ((key, value), rest) -> (
if structural_block_field key then loop acc rest
else
match Property.parse_key key with
| Some key ->
loop (Vec.push_back acc { Property.key; value }) rest
| None ->
Error
(Error.invalid_options
("invalid block property key: "
^ Melange_edn_melange.to_edn_string key)))
in
loop Vec.empty fields
let rec resolve_block_inline_properties invoke_config repo block =
let open Cli_effect in
match inline_property_assignments block with
| Error err -> pure (Error err)
| Ok assignments ->
bind (resolve_property_assignments invoke_config repo assignments)
(function
| Error err -> pure (Error (option_resolution_error "--blocks" err))
| Ok resolved_assignments ->
let resolved_properties =
Vec.map
(fun (ident, value) ->
{ Property.key = Property.Key_ident ident; value })
resolved_assignments
in
let rec resolve_children acc remaining =
match Vec.pop_front remaining with
| None -> pure (Ok acc)
| Some (child, rest) ->
bind
(resolve_block_inline_properties invoke_config repo child)
(function
| Error err -> pure (Error err)
| Ok child ->
resolve_children (Vec.push_back acc child) rest)
in
bind (resolve_children Vec.empty block.Block.children) (function
| Error err -> pure (Error err)
| Ok children ->
pure
(Ok
{
block with
Block.properties =
Vec.append block.Block.properties resolved_properties;
children;
})))
let resolve_blocks_inline_properties invoke_config repo blocks =
let open Cli_effect in
let rec loop acc remaining =
match Vec.pop_front remaining with
| None -> pure (Ok acc)
| Some (block, rest) ->
bind (resolve_block_inline_properties invoke_config repo block)
(function
| Error err -> pure (Error err)
| Ok block -> loop (Vec.push_back acc block) rest)
in
loop Vec.empty blocks
let rec strip_block_properties block =
{
block with
Block.properties = Vec.empty;
children = Vec.map strip_block_properties block.Block.children;
}
let property_key_value = function
| Property.Key_ident ident -> Edn_util.any ident
| Key_id id -> Edn_util.int64 id
| Key_name name -> Edn_util.string name
let rec block_inline_property_ops block =
let own_ops =
match (block.Block.uuid, block.Block.properties) with
| Some uuid, properties ->
Vec.map
(fun assignment ->
Edn_util.vector_vec
(Vec.of_array
[|
kw "batch-set-property";
Edn_util.vector_vec
(Vec.of_array
[|
Edn_util.vector_vec
(Vec.of_array [| Edn_util.uuid uuid |]);
property_key_value assignment.Property.key;
assignment.value;
Edn_util.map_vec Vec.empty;
|]);
|]))
properties
| None, _ -> Vec.empty
in
Vec.append own_ops
(Vec.concat_map block_inline_property_ops block.Block.children)
let inline_property_ops blocks = Vec.concat_map block_inline_property_ops blocks
let apply_inline_property_ops invoke_config repo blocks =
let open Cli_effect in
let ops = inline_property_ops blocks in
if Vec.is_empty ops then pure (Ok Edn_util.nil)
else
bind (apply_outliner_ops invoke_config repo ops) (fun result ->
pure (Ok result))
let append_tag_and_property_ops block_uuids ~update_tag_ids ~remove_tag_ids
~update_properties ~remove_properties =
let uuid_values =
Edn_util.vector_vec (block_uuids |> Vec.map (fun uuid -> Edn_util.uuid uuid))
in
let remove_tag_ops =
Vec.map
(fun tag_id ->
Edn_util.vector_vec
(Vec.of_array
[|
kw "batch-delete-property-value";
Edn_util.vector_vec
(Vec.of_array
[| uuid_values; kw "block/tags"; Edn_util.int64 tag_id |]);
|]))
remove_tag_ids
in
let remove_property_ops =
Vec.map
(fun ident ->
Edn_util.vector_vec
(Vec.of_array
[|
kw "batch-remove-property";
Edn_util.vector_vec
(Vec.of_array [| uuid_values; Edn_util.any ident |]);
|]))
remove_properties
in
let update_tag_ops =
Vec.map
(fun tag_id ->
Edn_util.vector_vec
(Vec.of_array
[|
kw "batch-set-property";
Edn_util.vector_vec
(Vec.of_array
[|
uuid_values;
kw "block/tags";
Edn_util.int64 tag_id;
Edn_util.map_vec Vec.empty;
|]);
|]))
update_tag_ids
in
let update_property_ops =
Vec.map
(fun (ident, value) ->
Edn_util.vector_vec
(Vec.of_array
[|
kw "batch-set-property";
Edn_util.vector_vec
(Vec.of_array
[|
uuid_values;
Edn_util.any ident;
value;
Edn_util.map_vec Vec.empty;
|]);
|]))
update_properties
in
Vec.append remove_tag_ops
(Vec.append remove_property_ops
(Vec.append update_tag_ops update_property_ops))
let resolve_update_plan invoke_config repo plan =
let open Cli_effect in
bind (resolve_tag_ids invoke_config repo plan.Property.update_tags) (function
| Error err -> pure (Error (option_resolution_error "--update-tags" err))
| Ok update_tag_ids ->
bind (resolve_tag_ids invoke_config repo plan.remove_tags) (function
| Error err ->
pure (Error (option_resolution_error "--remove-tags" err))
| Ok remove_tag_ids ->
bind
(resolve_property_assignments invoke_config repo
plan.update_properties) (function
| Error err ->
pure
(Error (option_resolution_error "--update-properties" err))
| Ok update_properties ->
bind
(resolve_property_keys invoke_config repo
plan.remove_properties) (function
| Error err ->
pure
(Error
(option_resolution_error "--remove-properties"
err))
| Ok remove_properties ->
pure
(Ok
( update_tag_ids,
remove_tag_ids,
update_properties,
remove_properties ))))))
let apply_resolved_update_plan invoke_config repo block_uuids
(update_tag_ids, remove_tag_ids, update_properties, remove_properties) =
let open Cli_effect in
let ops =
append_tag_and_property_ops block_uuids ~update_tag_ids ~remove_tag_ids
~update_properties ~remove_properties
in
if Vec.is_empty ops then pure (Ok Edn_util.nil)
else
bind (apply_outliner_ops invoke_config repo ops) (fun result ->
pure (Ok result))
let execute_plan_on_uuids invoke_config repo block_uuids plan =
let open Cli_effect in
bind (resolve_update_plan invoke_config repo plan) (function
| Error err -> pure (Error err)
| Ok resolved ->
apply_resolved_update_plan invoke_config repo block_uuids resolved)
let execute_page_plan invoke_config repo page_uuid plan =
execute_plan_on_uuids invoke_config repo (Vec.singleton page_uuid) plan
let restore_recycled_page invoke_config repo page_uuid =
apply_outliner_ops invoke_config repo
(Vec.singleton
(vector_vec
(Vec.of_array
[|
kw "restore-recycled";
vector_vec (Vec.of_array [| Edn_util.uuid page_uuid |]);
|])))
let pull_created_page invoke_config repo name create_result =
let uuid_value =
match
(Edn_util.as_vector create_result, Edn_util.as_list create_result)
with
| Some values, _ -> Vec.nth_opt values 1
| _, Some values -> Vec.nth_opt values 1
| _ -> None
in
match uuid_value with
| Some uuid_value -> (
match Edn_util.as_string_like uuid_value with
| Some uuid ->
Transport.thread_api_pull invoke_config ~repo
~selector:
(Edn_util.vector_t_vec
(Vec.of_array [| kw "db/id"; kw "block/uuid" |]))
~lookup:
(vector_vec
(Vec.of_array [| kw "block/uuid"; Edn_util.uuid uuid |]))
| _ ->
Transport.thread_api_pull invoke_config ~repo
~selector:
(Edn_util.vector_t_vec
(Vec.of_array [| kw "db/id"; kw "block/uuid" |]))
~lookup:
(vector_vec
(Vec.of_array
[|
kw "block/name";
Edn_util.string (normalized_lookup_name name);
|])))
| _ ->
Transport.thread_api_pull invoke_config ~repo
~selector:
(Edn_util.vector_t_vec
(Vec.of_array [| kw "db/id"; kw "block/uuid" |]))
~lookup:
(vector_vec
(Vec.of_array
[|
kw "block/name"; Edn_util.string (normalized_lookup_name name);
|]))
let tag_schema_property_ops tag_uuid ~add_properties ~remove_properties =
let add_ops =
Vec.map
(fun ident ->
Edn_util.vector_vec
(Vec.of_array
[|
kw "class-add-property";
Edn_util.vector_vec
(Vec.of_array [| Edn_util.uuid tag_uuid; Edn_util.any ident |]);
|]))
add_properties
in
let remove_ops =
Vec.map
(fun ident ->
Edn_util.vector_vec
(Vec.of_array
[|
kw "class-remove-property";
Edn_util.vector_vec
(Vec.of_array [| Edn_util.uuid tag_uuid; Edn_util.any ident |]);
|]))
remove_properties
in
Vec.append add_ops remove_ops
let execute_tag_schema_properties invoke_config repo tag_uuid ~add_properties
~remove_properties =
let open Cli_effect in
bind (resolve_schema_property_keys invoke_config repo add_properties)
(function
| Error err -> pure (Error (option_resolution_error "--add-properties" err))
| Ok add_properties ->
bind (resolve_schema_property_keys invoke_config repo remove_properties)
(function
| Error err ->
pure (Error (option_resolution_error "--remove-properties" err))
| Ok remove_properties ->
let ops =
tag_schema_property_ops tag_uuid ~add_properties
~remove_properties
in
if Vec.is_empty ops then pure (Ok Edn_util.nil)
else
bind (apply_outliner_ops invoke_config repo ops) (fun result ->
pure (Ok result))))
let tag_result_with_schema_properties mode invoke_config repo entity
~add_properties ~remove_properties =
let open Cli_effect in
match (id_of_entity entity, uuid_of_entity entity) with
| Some id, Some uuid ->
if tag_entity entity then
bind
(execute_tag_schema_properties invoke_config repo uuid ~add_properties
~remove_properties) (function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_tag mode err)
| Ok _ -> pure (ok_tag_ids mode (Vec.singleton id)))
else
pure
(Cli_result.error ~command:Command_id.Upsert_tag mode
(Error.make Error.Tag_create_not_tag
"created entity is not tagged as :logseq.class/Tag"))
| Some _, None ->
pure
(Cli_result.error ~command:Command_id.Upsert_tag mode
(Error.make Error.Upsert_id_not_found "tag uuid not found"))
| None, _ -> pure (tag_not_found mode)
let execute_create_tag mode invoke_config repo name ~add_properties
~remove_properties =
let open Cli_effect in
bind (pull_tag_by_name invoke_config repo name tag_selector) (fun existing ->
match first_entity existing with
| Some entity ->
tag_result_with_schema_properties mode invoke_config repo entity
~add_properties ~remove_properties
| None ->
bind (create_tag_page invoke_config repo name) (fun _ ->
bind (pull_tag_by_name invoke_config repo name tag_selector)
(fun page ->
match first_entity page with
| Some entity ->
tag_result_with_schema_properties mode invoke_config repo
entity ~add_properties ~remove_properties
| None -> pure (tag_not_found mode))))
let execute_update_tag mode invoke_config repo id name ~add_properties
~remove_properties =
let open Cli_effect in
bind (pull_entity_by_id invoke_config repo tag_selector id) (fun entity ->
match id_of_entity entity with
| None ->
pure
(Cli_result.error ~command:Command_id.Upsert_tag mode
(upsert_id_not_found "tag" id))
| Some id when not (tag_entity entity) ->
pure
(Cli_result.error ~command:Command_id.Upsert_tag mode
(upsert_id_type_mismatch "Tag" id))
| Some _ -> (
match name with
| None ->
tag_result_with_schema_properties mode invoke_config repo entity
~add_properties ~remove_properties
| Some target_name when same_tag_name entity target_name ->
tag_result_with_schema_properties mode invoke_config repo entity
~add_properties ~remove_properties
| Some target_name ->
bind
(pull_tag_by_name invoke_config repo target_name tag_selector)
(fun target ->
match
Option.bind (first_entity target) (rename_conflict entity)
with
| Some err ->
pure
(Cli_result.error ~command:Command_id.Upsert_tag mode
err)
| None -> (
match uuid_of_entity entity with
| None ->
pure
(Cli_result.error ~command:Command_id.Upsert_tag
mode
(Error.make Error.Upsert_id_not_found
"tag uuid not found"))
| Some uuid ->
bind (rename_page invoke_config repo uuid target_name)
(fun _ ->
tag_result_with_schema_properties mode
invoke_config repo entity ~add_properties
~remove_properties)))))
let execute_create_property mode invoke_config repo name schema =
let open Cli_effect in
bind (pull_property_by_name invoke_config repo name property_selector)
(fun existing ->
let ident = Option.bind (first_entity existing) ident_of_entity in
let opts =
match ident with
| Some _ -> Edn_util.map_vec Vec.empty
| None ->
Edn_util.map_vec
(Vec.of_array [| (kw "property-name", Edn_util.string name) |])
in
bind (upsert_property invoke_config repo ~opts ident schema) (fun _ ->
bind (pull_property_by_name invoke_config repo name property_selector)
(fun property ->
match Option.bind (first_entity property) id_of_entity with
| Some id -> pure (ok_property_ids mode (Vec.singleton id))
| None ->
pure
(Cli_result.error ~command:Command_id.Upsert_property mode
(Error.make Error.Property_not_found
"property not found after upsert")))))
let execute_update_property mode invoke_config repo id schema =
let open Cli_effect in
bind (pull_entity_by_id invoke_config repo property_selector id)
(fun entity ->
match id_of_entity entity with
| None ->
pure
(Cli_result.error ~command:Command_id.Upsert_property mode
(upsert_id_not_found "property" id))
| Some id when not (property_entity entity) ->
pure
(Cli_result.error ~command:Command_id.Upsert_property mode
(upsert_id_type_mismatch "Property" id))
| Some id ->
if schema_empty schema then
pure (ok_property_ids mode (Vec.singleton id))
else
let ident = ident_of_entity entity in
bind (upsert_property invoke_config repo ident schema) (fun _ ->
pure (ok_property_ids mode (Vec.singleton id))))
let save_block_title invoke_config repo uuid content =
let block =
Edn_util.map_vec
(Vec.of_array
[|
(kw "block/uuid", Edn_util.uuid uuid);
(kw "block/title", Edn_util.string content);
|])
in
let op =
Edn_util.vector_vec
(Vec.of_array
[|
kw "save-block";
Edn_util.vector_vec
(Vec.of_array [| block; Edn_util.map_vec Vec.empty |]);
|])
in
apply_outliner_ops invoke_config repo (Vec.singleton op)
let pull_asset_by_uuid invoke_config repo uuid =
pull_entity_by_lookup invoke_config repo asset_selector
(vector_vec (Vec.of_array [| kw "block/uuid"; Edn_util.uuid uuid |]))
let ensure_asset_file_metadata path =
if not (Cli_unix.file_exists path) then
Error
(Error.make
~context:
(Edn_util.map_vec
(Vec.of_array [| (kw "path", Edn_util.string path) |]))
Error.Asset_file_not_found "asset file not found")
else
match file_extension path with
| None ->
Error
(Error.make
~context:
(Edn_util.map_vec
(Vec.of_array [| (kw "path", Edn_util.string path) |]))
Error.Invalid_options "asset path must include a file extension")
| Some asset_type -> (
match file_sha256 path with
| None ->
Error
(Error.make
~context:
(Edn_util.map_vec
(Vec.of_array [| (kw "path", Edn_util.string path) |]))
Error.Asset_checksum_failed "asset checksum failed")
| Some checksum -> Ok (asset_type, file_size path, checksum))
let ensure_asset_tag_id invoke_config repo =
let open Cli_effect in
bind
(pull_entity_by_lookup invoke_config repo
(vector_vec (Vec.of_array [| kw "db/id" |]))
(vector_vec (Vec.of_array [| kw "db/ident"; kw "logseq.class/Asset" |])))
(fun entity ->
match id_of_entity entity with
| Some id -> pure (Ok id)
| None ->
pure
(Error (Error.make Error.Asset_tag_not_found "asset tag not found")))
let with_asset_metadata (block : Block.t) asset_tag_id asset_type asset_size
asset_checksum =
{
block with
Block.tags = Vec.push_front block.tags (Selector.Tag_id asset_tag_id);
properties =
Vec.prepend_array block.properties
[|
{
Property.key =
Property.Key_ident
(Edn_util.keyword_t "logseq.property.asset/type");
value = Edn_util.string asset_type;
};
{
key =
Property.Key_ident
(Edn_util.keyword_t "logseq.property.asset/size");
value = Edn_util.int64 (Int64.of_int asset_size);
};
{
key =
Property.Key_ident
(Edn_util.keyword_t "logseq.property.asset/checksum");
value = Edn_util.string asset_checksum;
};
|];
}
let result_ids_of_create_result result =
match Cli_result.data_value result with
| Some value -> (
match Option.bind (Edn_util.get value "result") Edn_util.as_seq with
| Some values ->
let ids = Vec.filter_map Edn_util.as_int64 values in
if Vec.length ids = Vec.length values then Some ids else None
| _ -> None)
| None -> None
let execute_create_asset mode config repo path create_action =
let open Cli_effect in
bind (Server_runtime.ensure_server config repo ~create_empty_db:false)
(function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_asset mode err)
| Ok invoke_config -> (
match ensure_asset_file_metadata path with
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_asset mode err)
| Ok (asset_type, asset_size, asset_checksum) ->
bind (ensure_asset_tag_id invoke_config repo) (function
| Error err ->
pure
(Cli_result.error ~command:Command_id.Upsert_asset mode err)
| Ok asset_tag_id -> (
match Vec.pop_front create_action.blocks with
| None ->
pure
(Cli_result.error ~command:Command_id.Upsert_asset mode
(Error.make Error.Asset_create_failed
"created asset block missing uuid"))
| Some (first, rest) -> (
match first.Block.uuid with
| None ->
pure
(Cli_result.error ~command:Command_id.Upsert_asset
mode
(Error.make Error.Asset_create_failed
"created asset block missing uuid"))
| Some block_uuid -> (
let first =
with_asset_metadata first asset_tag_id asset_type
asset_size asset_checksum
in
let create_action =
{
create_action with
blocks = Vec.push_front rest first;
}
in
let add_action =
add_action_of_block_create create_action
in
let destination =
Filename.concat
(graph_assets_dir config repo)
(block_uuid ^ "." ^ asset_type)
in
try
copy_file path destination;
bind (Add.execute_add_block add_action config mode)
(fun result ->
if Cli_result.is_error result then pure result
else
match result_ids_of_create_result result with
| Some ids ->
pure
(Cli_result.ok
~command:Command_id.Upsert_asset mode
(Raw (result_ids ids)))
| None ->
pure
(Cli_result.error
~command:Command_id.Upsert_asset mode
(Error.make Error.Asset_create_failed
"asset block not created")))
with exn ->
pure
(Cli_result.error ~command:Command_id.Upsert_asset
mode
(Error.make Error.Asset_file_copy_failed
(Printexc.to_string exn)))))))))
let execute_update_asset mode invoke_config repo id uuid content =
let open Cli_effect in
let pulled =
match (id, uuid) with
| Some id, _ -> pull_entity_by_id invoke_config repo asset_selector id
| None, Some uuid -> pull_asset_by_uuid invoke_config repo uuid
| None, None -> pure Edn_util.nil
in
bind pulled (fun entity ->
match id_of_entity entity with
| None ->
pure
(Cli_result.error ~command:Command_id.Upsert_asset mode
(upsert_id_not_found "asset" (Option.value id ~default:0L)))
| Some id when not (asset_entity entity) ->
pure
(Cli_result.error ~command:Command_id.Upsert_asset mode
(upsert_id_type_mismatch "Asset" id))
| Some id -> (
match (content, uuid_of_entity entity) with
| Some content, Some uuid ->
bind (save_block_title invoke_config repo uuid content) (fun _ ->
pure (ok_asset_ids mode (Vec.singleton id)))
| Some _, None ->
pure
(Cli_result.error ~command:Command_id.Upsert_asset mode
(Error.make Error.Upsert_id_not_found "asset uuid not found"))
| None, _ -> pure (ok_asset_ids mode (Vec.singleton id))))
let ok_task_ids mode ids =
Cli_result.ok ~command:Command_id.Upsert_task mode (Raw (result_ids ids))
let status_query invoke_config repo =
Transport.thread_api_q invoke_config ~repo
~query:
(Edn_util.vector_t_vec
(Vec.of_array
[| Edn_util.any Task_status.status_closed_values_query |]))
let resolve_task_status invoke_config repo = function
| None -> Cli_effect.pure (Ok None)
| Some status_input ->
let open Cli_effect in
bind (status_query invoke_config repo) (fun result ->
let values =
Option.value (Edn_util.as_seq result) ~default:Vec.empty
in
let statuses = Task_status.normalize_available_statuses values in
match Task_status.resolve_status_ident status_input statuses with
| Some ident -> pure (Ok (Some ident))
| None ->
pure
(Error
(Error.invalid_options
(Task_status.invalid_status_message status_input statuses))))
let ensure_task_tag_id invoke_config repo =
let open Cli_effect in
bind
(pull_entity_by_lookup invoke_config repo
(vector_vec (Vec.of_array [| kw "db/id" |]))
(vector_vec (Vec.of_array [| kw "db/ident"; kw "logseq.class/Task" |])))
(fun entity ->
match id_of_entity entity with
| Some id -> pure (Ok id)
| None ->
pure
(Error (Error.make Error.Task_tag_not_found "task tag not found")))
let ensure_task_page invoke_config repo page =
let open Cli_effect in
bind (pull_pages_by_name invoke_config repo page task_selector) (fun result ->
match first_entity result with
| Some entity -> (
match (id_of_entity entity, uuid_of_entity entity) with
| Some id, Some uuid -> pure (Ok (id, uuid))
| _ ->
pure
(Error
(Error.make Error.Page_not_found
"page not found after upsert")))
| None ->
bind (create_page invoke_config repo page) (fun create_result ->
bind (pull_created_page invoke_config repo page create_result)
(fun entity ->
match (id_of_entity entity, uuid_of_entity entity) with
| Some id, Some uuid -> pure (Ok (id, uuid))
| _ ->
pure
(Error
(Error.make Error.Page_not_found
"page not found after upsert")))))
let execute_task_plan_on_uuids invoke_config repo block_uuids task_tag_id
status_ident update_property_assignments clear_properties =
let update_properties =
match status_ident with
| Some ident ->
Vec.singleton
(Edn_util.keyword_t "logseq.property/status", Edn_util.any ident)
| None -> Vec.empty
in
let update_properties =
Vec.append update_properties
(Vec.filter_map
(fun assignment ->
match assignment.Property.key with
| Property.Key_ident ident -> Some (ident, assignment.value)
| _ -> None)
update_property_assignments)
in
let remove_properties =
Vec.filter_map
(function Property.Key_ident ident -> Some ident | _ -> None)
clear_properties
in
apply_resolved_update_plan invoke_config repo block_uuids
(Vec.singleton task_tag_id, Vec.empty, update_properties, remove_properties)
let execute_task_plan_on_uuid invoke_config repo block_uuid task_tag_id
status_ident update_property_assignments clear_properties =
execute_task_plan_on_uuids invoke_config repo (Vec.singleton block_uuid)
task_tag_id status_ident update_property_assignments clear_properties
let execute_task_page mode invoke_config repo page status_input
update_properties clear_properties =
let open Cli_effect in
bind (resolve_task_status invoke_config repo status_input) (function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_task mode err)
| Ok status_ident ->
bind (ensure_task_tag_id invoke_config repo) (function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_task mode err)
| Ok task_tag_id ->
bind (ensure_task_page invoke_config repo page) (function
| Error err ->
pure
(Cli_result.error ~command:Command_id.Upsert_task mode err)
| Ok (page_id, page_uuid) ->
bind
(execute_task_plan_on_uuid invoke_config repo page_uuid
task_tag_id status_ident update_properties
clear_properties) (function
| Error err ->
pure
(Cli_result.error ~command:Command_id.Upsert_task
mode err)
| Ok _ -> pure (ok_task_ids mode (Vec.singleton page_id))))))
let pull_task_by_uuid invoke_config repo uuid =
pull_entity_by_lookup invoke_config repo task_selector
(vector_vec (Vec.of_array [| kw "block/uuid"; Edn_util.uuid uuid |]))
let execute_task_update mode invoke_config repo id uuid status_input
update_properties clear_properties =
let open Cli_effect in
bind (resolve_task_status invoke_config repo status_input) (function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_task mode err)
| Ok status_ident ->
bind (ensure_task_tag_id invoke_config repo) (function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_task mode err)
| Ok task_tag_id ->
let pulled =
match (id, uuid) with
| Some id, _ ->
pull_entity_by_id invoke_config repo task_selector id
| None, Some uuid -> pull_task_by_uuid invoke_config repo uuid
| None, None -> pure Edn_util.nil
in
bind pulled (fun entity ->
match (id_of_entity entity, uuid_of_entity entity) with
| Some node_id, Some node_uuid ->
bind
(execute_task_plan_on_uuid invoke_config repo node_uuid
task_tag_id status_ident update_properties
clear_properties) (function
| Error err ->
pure
(Cli_result.error ~command:Command_id.Upsert_task
mode err)
| Ok _ ->
pure (ok_task_ids mode (Vec.singleton node_id)))
| Some _, None ->
pure
(Cli_result.error ~command:Command_id.Upsert_task mode
(Error.make Error.Upsert_id_not_found
"task uuid not found"))
| None, _ ->
pure
(Cli_result.error ~command:Command_id.Upsert_task mode
(upsert_id_not_found "task"
(Option.value id ~default:0L))))))
let page_result_with_plan ?(restore = false) mode invoke_config repo page_id
page_uuid plan =
let open Cli_effect in
let restore_effect =
if restore then restore_recycled_page invoke_config repo page_uuid
else pure Edn_util.nil
in
bind restore_effect (fun _ ->
bind (execute_page_plan invoke_config repo page_uuid plan) (function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_page mode err)
| Ok _ -> pure (ok_page_ids mode (Vec.singleton page_id))))
let execute_create_page mode invoke_config repo name restore plan =
let open Cli_effect in
bind (pull_pages_by_name invoke_config repo name page_selector)
(fun existing ->
match first_entity existing with
| Some page -> (
let recycled = recycled_entity page in
if recycled && not restore then
pure
(Cli_result.error ~command:Command_id.Upsert_page mode
(recycled_page_error ()))
else
match (id_of_entity page, uuid_of_entity page) with
| Some id, Some uuid ->
page_result_with_plan ~restore:recycled mode invoke_config repo
id uuid plan
| Some _, None when recycled ->
pure
(Cli_result.error ~command:Command_id.Upsert_page mode
(recycled_page_error ()))
| Some id, None -> pure (ok_page_ids mode (Vec.singleton id))
| _ ->
pure
(Cli_result.error ~command:Command_id.Upsert_page mode
(page_not_found ())))
| None ->
bind (create_page invoke_config repo name) (fun create_result ->
bind (pull_created_page invoke_config repo name create_result)
(fun page ->
match (id_of_entity page, uuid_of_entity page) with
| Some id, Some uuid ->
page_result_with_plan mode invoke_config repo id uuid plan
| Some id, None -> pure (ok_page_ids mode (Vec.singleton id))
| _ ->
pure
(Cli_result.error ~command:Command_id.Upsert_page mode
(page_not_found ())))))
let execute_update_page mode invoke_config repo id restore plan =
let open Cli_effect in
bind (pull_entity_by_id invoke_config repo page_selector id) (fun entity ->
match id_of_entity entity with
| None ->
pure
(Cli_result.error ~command:Command_id.Upsert_page mode
(upsert_id_not_found "page" id))
| Some id when not (page_entity entity) ->
pure
(Cli_result.error ~command:Command_id.Upsert_page mode
(upsert_id_type_mismatch "Page" id))
| Some _ when recycled_entity entity && not restore ->
pure
(Cli_result.error ~command:Command_id.Upsert_page mode
(recycled_page_error ()))
| Some id -> (
let recycled = recycled_entity entity in
match uuid_of_entity entity with
| Some uuid ->
page_result_with_plan ~restore:recycled mode invoke_config repo id
uuid plan
| None when recycled ->
pure
(Cli_result.error ~command:Command_id.Upsert_page mode
(recycled_page_error ()))
| None -> pure (ok_page_ids mode (Vec.singleton id))))
let update_plan_empty (plan : Property.update_plan) =
Vec.is_empty plan.update_tags
&& Vec.is_empty plan.remove_tags
&& Vec.is_empty plan.update_properties
&& Vec.is_empty plan.remove_properties
let result_ids_of_cli_result result =
match Cli_result.data_value result with
| Some value -> (
match Option.bind (Edn_util.get value "result") Edn_util.as_seq with
| Some values ->
let ids = Vec.filter_map Edn_util.as_int64 values in
if Vec.length ids = Vec.length values then Some ids else None
| _ -> None)
| _ -> None
let resolve_block_uuids_by_id invoke_config repo ids =
let open Cli_effect in
let rec loop acc remaining =
match Vec.pop_front remaining with
| None -> pure (Ok acc)
| Some (id, rest) ->
bind
(pull_entity_by_id invoke_config repo
(vector_vec (Vec.of_array [| kw "db/id"; kw "block/uuid" |]))
id)
(fun entity ->
match uuid_of_entity entity with
| None -> pure (Error (upsert_id_not_found "block" id))
| Some uuid -> loop (Vec.push_back acc uuid) rest)
in
loop Vec.empty ids
let block_uuids_of_add_action action =
Vec.filter_map (fun block -> block.Block.uuid) action.Add.blocks
let execute_task_create mode config invoke_config repo target_page content
status_input update_properties clear_properties =
let open Cli_effect in
bind (resolve_task_status invoke_config repo status_input) (function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_task mode err)
| Ok status_ident ->
bind (ensure_task_tag_id invoke_config repo) (function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_task mode err)
| Ok task_tag_id -> (
let add_opts =
{
Add.target_id = None;
target_uuid = None;
target_page_name = Some target_page;
pos = None;
status = None;
tags_edn = None;
properties_edn = None;
content = Some content;
blocks_edn = None;
blocks_file = None;
}
in
match Add.build_add_block_action add_opts Vec.empty repo with
| Error err ->
pure
(Cli_result.error ~command:Command_id.Upsert_task mode err)
| Ok create_action ->
bind (Add.execute_add_block create_action config mode)
(fun result ->
if Cli_result.is_error result then
pure
(Cli_result.with_command Command_id.Upsert_task result)
else
match result_ids_of_cli_result result with
| None ->
pure
(Cli_result.error ~command:Command_id.Upsert_task
mode
(Error.make Error.Add_id_resolution_failed
"unable to resolve created ids"))
| Some ids ->
let block_uuids =
block_uuids_of_add_action create_action
in
let resolved_uuids =
if Vec.is_empty block_uuids then
resolve_block_uuids_by_id invoke_config repo ids
else pure (Ok block_uuids)
in
bind resolved_uuids (function
| Error err ->
pure
(Cli_result.error
~command:Command_id.Upsert_task mode err)
| Ok block_uuids ->
bind
(execute_task_plan_on_uuids invoke_config
repo block_uuids task_tag_id status_ident
update_properties clear_properties)
(function
| Error err ->
pure
(Cli_result.error
~command:Command_id.Upsert_task
mode err)
| Ok _ -> pure (ok_task_ids mode ids)))))))
let execute_create_block mode (action : block_create) config =
let open Cli_effect in
bind (Server_runtime.ensure_server config action.repo ~create_empty_db:false)
(function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_block mode err)
| Ok invoke_config ->
bind
(resolve_blocks_inline_properties invoke_config action.repo
action.blocks) (function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_block mode err)
| Ok blocks ->
let action = { action with blocks } in
let resolved_plan_effect =
if update_plan_empty action.update_plan then pure (Ok None)
else
map
(function
| Ok plan -> Ok (Some plan) | Error err -> Error err)
(resolve_update_plan invoke_config action.repo
action.update_plan)
in
bind resolved_plan_effect (function
| Error err ->
pure
(Cli_result.error ~command:Command_id.Upsert_block mode
err)
| Ok resolved_plan ->
let add_action =
add_action_of_block_create
{
action with
blocks = Vec.map strip_block_properties action.blocks;
}
in
bind (Add.execute_add_block add_action config mode)
(fun result ->
if Cli_result.is_error result then pure result
else
bind
(apply_inline_property_ops invoke_config action.repo
action.blocks) (function
| Error err ->
pure
(Cli_result.error
~command:Command_id.Upsert_block mode err)
| Ok _ -> (
match resolved_plan with
| None -> pure result
| Some resolved_plan -> (
match result_ids_of_cli_result result with
| None ->
pure
(Cli_result.error
~command:Command_id.Upsert_block
mode
(Error.make
Error.Add_id_resolution_failed
"unable to resolve created ids"))
| Some ids ->
bind
(resolve_block_uuids_by_id
invoke_config action.repo ids)
(function
| Error err ->
pure
(Cli_result.error
~command:
Command_id.Upsert_block
mode err)
| Ok block_uuids ->
bind
(apply_resolved_update_plan
invoke_config action.repo
block_uuids resolved_plan)
(function
| Error err ->
pure
(Cli_result.error
~command:
Command_id
.Upsert_block mode
err)
| Ok _ -> pure result)))))))))
let execute_with_mode action config mode =
let open Cli_effect in
match action with
| Upsert_block (Block_create action) ->
execute_create_block mode action config
| Upsert_block (Block_update action) ->
map
(Cli_result.with_command Command_id.Upsert_block)
(Update.execute (update_action_of_block_update action) config mode)
| Upsert_tag
{
repo;
mode = Create;
name = Some name;
add_properties;
remove_properties;
_;
} ->
bind (Server_runtime.ensure_server config repo ~create_empty_db:false)
(function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_tag mode err)
| Ok invoke_config ->
execute_create_tag mode invoke_config repo name ~add_properties
~remove_properties)
| Upsert_tag
{
repo;
mode = Update;
id = Some id;
name;
add_properties;
remove_properties;
_;
} ->
bind (Server_runtime.ensure_server config repo ~create_empty_db:false)
(function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_tag mode err)
| Ok invoke_config ->
execute_update_tag mode invoke_config repo id name ~add_properties
~remove_properties)
| Upsert_tag _ ->
pure
(Cli_result.error ~command:Command_id.Upsert_tag mode
(Error.make Error.Not_implemented "upsert tag is not implemented"))
| Upsert_page { repo; mode = Create; page = Some page; restore; plan; _ } ->
bind (Server_runtime.ensure_server config repo ~create_empty_db:false)
(function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_page mode err)
| Ok invoke_config ->
execute_create_page mode invoke_config repo page restore plan)
| Upsert_page { repo; mode = Update; id = Some id; restore; plan; _ } ->
bind (Server_runtime.ensure_server config repo ~create_empty_db:false)
(function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_page mode err)
| Ok invoke_config ->
execute_update_page mode invoke_config repo id restore plan)
| Upsert_page _ ->
pure
(Cli_result.error ~command:Command_id.Upsert_page mode
(Error.make Error.Not_implemented "upsert page is not implemented"))
| Upsert_property { repo; mode = Create; name = Some name; schema; _ } ->
bind (Server_runtime.ensure_server config repo ~create_empty_db:false)
(function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_property mode err)
| Ok invoke_config ->
execute_create_property mode invoke_config repo name schema)
| Upsert_property { repo; mode = Update; id = Some id; schema; _ } ->
bind (Server_runtime.ensure_server config repo ~create_empty_db:false)
(function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_property mode err)
| Ok invoke_config ->
execute_update_property mode invoke_config repo id schema)
| Upsert_property _ ->
pure
(Cli_result.error ~command:Command_id.Upsert_property mode
(Error.make Error.Not_implemented
"upsert property is not implemented"))
| Upsert_asset { repo; mode = Update; id; uuid; content; _ } ->
bind (Server_runtime.ensure_server config repo ~create_empty_db:false)
(function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_asset mode err)
| Ok invoke_config ->
execute_update_asset mode invoke_config repo id uuid content)
| Upsert_asset
{
repo;
mode = Create;
path = Some path;
create_action = Some create_action;
_;
} ->
execute_create_asset mode config repo path create_action
| Upsert_asset _ ->
pure
(Cli_result.error ~command:Command_id.Upsert_asset mode
(Error.make Error.Not_implemented "upsert asset is not implemented"))
| Upsert_task
{
repo;
mode = Page;
page = Some page;
status_input;
update_properties;
clear_properties;
_;
} ->
bind (Server_runtime.ensure_server config repo ~create_empty_db:false)
(function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_task mode err)
| Ok invoke_config ->
execute_task_page mode invoke_config repo page status_input
update_properties clear_properties)
| Upsert_task
{
repo;
mode = Create;
page = Some target_page;
content = Some content;
status_input;
update_properties;
clear_properties;
_;
} ->
bind (Server_runtime.ensure_server config repo ~create_empty_db:false)
(function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_task mode err)
| Ok invoke_config ->
execute_task_create mode config invoke_config repo target_page
content status_input update_properties clear_properties)
| Upsert_task
{
repo;
mode = Update;
id;
uuid;
status_input;
update_properties;
clear_properties;
_;
} ->
bind (Server_runtime.ensure_server config repo ~create_empty_db:false)
(function
| Error err ->
pure (Cli_result.error ~command:Command_id.Upsert_task mode err)
| Ok invoke_config ->
execute_task_update mode invoke_config repo id uuid status_input
update_properties clear_properties)
| Upsert_task _ ->
pure
(Cli_result.error ~command:Command_id.Upsert_task mode
(Error.make Error.Not_implemented "upsert task is not implemented"))
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 upsert block --graph my-graph --target-page Home \
--content \"New block\"";
"logseq upsert block --graph my-graph --id 123 --content \
\"Updated content\"";
"logseq upsert block --graph my-graph --id 123 --target-page \
Home";
"logseq upsert block --graph my-graph --target-page Meeting \
Notes --content \"AI summary of the discussion\" --update-tags \
'[\"AI-GENERATED\"]'";
"logseq upsert block --graph my-graph --blocks '[{:block/title \
\"A\"} {:block/title \"B\"}]'";
|])
Command_id.Upsert_block "Upsert block";
meta
~examples:
(Vec.of_array
[|
"logseq upsert page --graph my-graph --page Home --update-tags \
'[\"project\"]'";
"logseq upsert page --graph my-graph --id 999 \
--update-properties '{:logseq.property/description \
\"Example\"}'";
|])
Upsert_page "Upsert page";
meta
~examples:
(Vec.of_array
[|
"logseq upsert task --graph my-graph --content \"Ship release\" \
--target-page Home --status todo --priority high --scheduled \
\"2026-02-10T08:00:00.000Z\" --deadline \
\"2026-02-12T18:00:00.000Z\"";
"logseq upsert task --graph my-graph --page Weekly Plan \
--status doing";
"logseq upsert task --graph my-graph --id 123 --no-status \
--no-priority";
|])
Upsert_task "Upsert task";
meta
~examples:
(Vec.of_array
[|
"logseq upsert asset --graph my-graph --path ./assets/logo.png \
--target-page Home";
"logseq upsert asset --graph my-graph --id 123 --content \
\"Updated asset title\"";
|])
Upsert_asset "Upsert asset";
meta
~examples:
(Vec.of_array
[|
"logseq upsert tag --graph my-graph --name project";
"logseq upsert tag --graph my-graph --id 200 --name Project \
Renamed";
"logseq upsert tag --graph my-graph --name project \
--add-properties '[\"status\" \"owner\"]'";
|])
Upsert_tag "Upsert tag";
meta
~examples:
(Vec.of_array
[|
"logseq upsert property --graph my-graph --name status --type \
default --cardinality one";
"logseq upsert property --graph my-graph --id 321 --hide true";
|])
Upsert_property "Upsert property";
|]
let repo = function
| Upsert_block (Block_create a) -> a.repo
| Upsert_block (Block_update a) -> a.repo
| Upsert_page { repo; _ }
| Upsert_task { repo; _ }
| Upsert_asset { repo; _ }
| Upsert_tag { repo; _ }
| Upsert_property { repo; _ } ->
repo
let graph = function
| Upsert_block (Block_create a) -> a.graph
| Upsert_block (Block_update a) -> a.graph
| Upsert_page { graph; _ }
| Upsert_task { graph; _ }
| Upsert_asset { graph; _ }
| Upsert_tag { graph; _ }
| Upsert_property { graph; _ } ->
graph
let execute action config =
let (Output.Mode.Packed mode) = Output_mode.for_config config in
execute_with_mode action config mode