type order = Asc | Desc type common_opts = { fields : string Rrbvec.t option; limit : int option; offset : int option; sort : string option; order : order option; } type page_opts = { common : common_opts; expand : bool; include_built_in : bool option; include_journal : bool option; journal_only : bool; include_hidden : bool; updated_after : Js.Date.t option; created_after : Js.Date.t option; } type tag_opts = { common : common_opts; expand : bool; include_built_in : bool option; with_properties : bool; with_extends : bool; } type property_opts = { common : common_opts; expand : bool; include_built_in : bool option; with_classes : bool; with_type : bool; } type task_opts = { common : common_opts; status : string option; priority : string option; content : string option; } type node_opts = { common : common_opts; tags : string Rrbvec.t; properties : string Rrbvec.t; } type asset_opts = { common : common_opts } type parsed = | Parsed_page of page_opts | Parsed_tag of tag_opts | Parsed_property of property_opts | Parsed_task of task_opts | Parsed_node of node_opts | Parsed_asset of asset_opts type kind = Page | Tag | Property | Task | Node | Asset type action = { kind : kind; command : Command_id.t; repo : Cli_primitive.repo; graph : Cli_primitive.graph; options : Melange_edn_melange.any; } type list_result = { items : Entity.t Rrbvec.t } let order_of_string value = match String.lowercase_ascii (String.trim value) with | "asc" -> Some Asc | "desc" -> Some Desc | _ -> None let string_of_order = function Asc -> "asc" | Desc -> "desc" let kind_of_parsed = function | Parsed_page _ -> Page | Parsed_tag _ -> Tag | Parsed_property _ -> Property | Parsed_task _ -> Task | Parsed_node _ -> Node | Parsed_asset _ -> Asset let normalize_priority value = match String.lowercase_ascii (String.trim value) with | "low" -> Some "logseq.property/priority.low" | "medium" -> Some "logseq.property/priority.medium" | "high" -> Some "logseq.property/priority.high" | "urgent" -> Some "logseq.property/priority.urgent" | _ -> None let invalid_priority_message value = "Invalid value for option :priority: " ^ value ^ ". Available values: low, medium, high, urgent" let validate_parsed = function | Parsed_page opts when opts.include_journal = Some true && opts.journal_only -> Error (Error.invalid_options "include-journal and journal-only are mutually exclusive") | Parsed_task opts -> ( match opts.priority with | Some value when normalize_priority value = None -> Error (Error.invalid_options (invalid_priority_message value)) | _ -> Ok ()) | Parsed_node opts when Vec.is_empty opts.tags && Vec.is_empty opts.properties -> Error (Error.invalid_options "list node requires at least one of --tags or --properties") | _ -> Ok () let normalize_options p = Error.bind (validate_parsed p) (fun () -> Ok p) let default_sort_field = "updated-at" let field_map fields = Vec.map (fun (name, keyword) -> (name, Edn_util.keyword_t keyword)) fields let page_field_map = field_map (Vec.of_array [| ("id", "db/id"); ("ident", "db/ident"); ("title", "block/title"); ("uuid", "block/uuid"); ("created-at", "block/created-at"); ("updated-at", "block/updated-at"); |]) let tag_field_map = Vec.append page_field_map (field_map (Vec.of_array [| ("properties", "logseq.property.class/properties"); ("extends", "logseq.property.class/extends"); ("description", "logseq.property/description"); |])) let property_field_map = Vec.append page_field_map (field_map (Vec.of_array [| ("classes", "logseq.property/classes"); ("type", "logseq.property/type"); ("cardinality", "db/cardinality"); ("description", "logseq.property/description"); |])) let task_field_map = field_map (Vec.of_array [| ("id", "db/id"); ("title", "block/title"); ("status", "logseq.property/status"); ("priority", "logseq.property/priority"); ("scheduled", "logseq.property/scheduled"); ("deadline", "logseq.property/deadline"); ("updated-at", "block/updated-at"); ("created-at", "block/created-at"); |]) let node_field_map = field_map (Vec.of_array [| ("id", "db/id"); ("title", "block/title"); ("type", "node/type"); ("page-id", "block/page-id"); ("page-title", "block/page-title"); ("created-at", "block/created-at"); ("updated-at", "block/updated-at"); |]) let asset_field_map = field_map (Vec.of_array [| ("id", "db/id"); ("title", "block/title"); ("asset-type", "logseq.property.asset/type"); ("size", "logseq.property.asset/size"); ("updated-at", "block/updated-at"); ("created-at", "block/created-at"); |]) let field_map_of_kind = function | Page -> page_field_map | Tag -> tag_field_map | Property -> property_field_map | Task -> task_field_map | Node -> node_field_map | Asset -> asset_field_map let value_rank value = match value with | Melange_edn_melange.Any Melange_edn_melange.Nil -> 0 | Any (Bool _) -> 1 | Any (Int _ | Bigint _ | Float _ | Decimal _ | Ratio _) -> 2 | Any (String _ | Symbol _ | Keyword _ | Tagged ("uuid", _)) -> 3 | Any (Tagged ("transit/bytes", _)) -> 4 | Any (List _ | Vector _ | Set _) -> 5 | Any (Map _) -> 6 | Any (Char _ | Regex _ | Tagged _) -> 7 let compare_value a b = let number value = match (Edn_util.as_int value, Edn_util.as_float value) with | Some value, _ -> Some (float_of_int value) | _, Some value -> Some value | _ -> None in match ( Edn_util.is_null a, Edn_util.is_null b, Edn_util.as_bool a, Edn_util.as_bool b, number a, number b, Edn_util.as_string_like a, Edn_util.as_string_like b, Edn_util.as_bytes a, Edn_util.as_bytes b ) with | true, true, _, _, _, _, _, _, _, _ -> 0 | _, _, Some a, Some b, _, _, _, _, _, _ -> Bool.compare a b | _, _, _, _, Some a, Some b, _, _, _, _ -> Float.compare a b | _, _, _, _, _, _, Some a, Some b, _, _ -> String.compare a b | _, _, _, _, _, _, _, _, Some a, Some b -> String.compare (Bytes.to_string a) (Bytes.to_string b) | _ -> let rank = Int.compare (value_rank a) (value_rank b) in if rank <> 0 then rank else String.compare (Melange_edn_melange.to_edn_string a) (Melange_edn_melange.to_edn_string b) let compare_item_by keyword a b = let value_of key item = Option.value (Edn_util.get item (Edn_util.keyword_to_string key)) ~default:Edn_util.nil in let primary = compare_value (value_of keyword a) (value_of keyword b) in if primary <> 0 then primary else compare_value (value_of (Edn_util.keyword_t "db/id") a) (value_of (Edn_util.keyword_t "db/id") b) let sort_values ~field_map common items = let sort_field = Option.value common.sort ~default:default_sort_field in match Vec.assoc_opt sort_field field_map with | None -> items | Some keyword -> ( let sorted = Vec.sort (compare_item_by keyword) items in match common.order with | Some Asc -> sorted | Some Desc | None -> Vec.rev sorted) let apply_offset_limit common entities = let entities = match common.offset with | Some n -> entities |> Vec.to_seq |> Seq.drop n |> Vec.of_seq | None -> entities in match common.limit with | Some n -> entities |> Vec.to_seq |> Seq.take n |> Vec.of_seq | None -> entities let command_id = function | Parsed_page _ -> Command_id.List_page | Parsed_tag _ -> List_tag | Parsed_property _ -> List_property | Parsed_task _ -> List_task | Parsed_node _ -> List_node | Parsed_asset _ -> List_asset let value_of_string_list values = Edn_util.vector_vec (values |> Vec.map (fun value -> Edn_util.string value)) let string_list_of_value value = match Edn_util.as_seq value with | Some values -> Vec.filter_map Edn_util.as_string_like values | None -> ( match Edn_util.as_string_like value with | Some value -> Vec.singleton value | None -> Vec.empty) let common_of_options options = { fields = Option.map string_list_of_value (Edn_util.get options "fields"); limit = Edn_util.get_int options "limit"; offset = Edn_util.get_int options "offset"; sort = Edn_util.get_string options "sort"; order = Option.bind (Edn_util.get_string options "order") order_of_string; } let select_fields field_map fields item = let keys = fields |> Vec.filter_map (fun field -> Vec.assoc_opt field field_map) in if Vec.is_empty keys then item else Edn_util.map_vec (keys |> Vec.filter_map (fun key -> Option.map (fun value -> (Edn_util.any key, value)) (Edn_util.get item (Edn_util.keyword_to_string key)))) let apply_fields field_map common items = match common.fields with | None -> items | Some fields -> Vec.map (select_fields field_map fields) items let postprocess_items kind options items = let common = common_of_options options in let field_map = field_map_of_kind kind in let sorted = sort_values ~field_map common items in let limited = sorted |> Vec.map Entity.of_value |> apply_offset_limit common |> Vec.map (fun entity -> entity.Entity.raw) in apply_fields field_map common limited 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 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 normalized_lookup_name value = String.lowercase_ascii (String.trim value) 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" |])); |]); kw "logseq.property/public?"; |]) let property_selector = vector_vec (Vec.of_array [| kw "db/id"; kw "db/ident"; kw "block/name"; kw "block/title"; kw "logseq.property/type"; kw "db/cardinality"; kw "logseq.property/public?"; |]) 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 "?tag" |])); Cli_primitive.V (Edn_util.vector_t_vec (Vec.of_array [| sym "?tag"; kw "db/ident"; kw class_ident |])); |]) () let pull_tag_by_name config repo name = Transport.thread_api_q config ~repo ~query: (Edn_util.vector_t_vec (Vec.of_array [| query_value (class_query tag_selector "logseq.class/Tag"); Edn_util.string (normalized_lookup_name name); |])) let pull_tag_by_uuid config repo uuid = Transport.thread_api_pull config ~repo ~selector:(Edn_util.expect_vector_t "list tag selector" tag_selector) ~lookup: (vector_vec (Vec.of_array [| kw "block/uuid"; Edn_util.uuid uuid |])) let pull_property_by_ident config repo ident = Transport.thread_api_pull config ~repo ~selector: (Edn_util.expect_vector_t "list property selector" property_selector) ~lookup:(vector_vec (Vec.of_array [| kw "db/ident"; kw ident |])) let pull_property_by_name config repo name = Transport.thread_api_q config ~repo ~query: (Edn_util.vector_t_vec (Vec.of_array [| query_value (class_query property_selector "logseq.class/Property"); Edn_util.string (normalized_lookup_name name); |])) let pull_property_by_id config repo id = Transport.thread_api_pull config ~repo ~selector: (Edn_util.expect_vector_t "list property selector" property_selector) ~lookup:(Edn_util.int64 id) let pull_asset_tag config repo = Transport.thread_api_pull config ~repo ~selector:(Edn_util.vector_t_vec (Vec.of_array [| kw "db/id" |])) ~lookup: (vector_vec (Vec.of_array [| kw "db/ident"; kw "logseq.class/Asset" |])) let first_entity value = let map_value value = match Edn_util.as_map value with Some _ -> Some value | None -> None in match (Edn_util.as_vector value, Edn_util.as_list value, Edn_util.as_map value) with | Some values, _, _ when not (Vec.is_empty values) -> ( let first = Vec.peek_front values in match map_value first with | Some _ as result -> result | None -> ( match (Edn_util.as_vector first, Edn_util.as_list first) with | Some nested_values, _ when not (Vec.is_empty nested_values) -> map_value (Vec.peek_front nested_values) | _, Some nested_values when not (Vec.is_empty nested_values) -> map_value (Vec.peek_front nested_values) | _ -> None)) | _, Some values, _ when not (Vec.is_empty values) -> ( let first = Vec.peek_front values in match map_value first with | Some _ as result -> result | None -> ( match (Edn_util.as_vector first, Edn_util.as_list first) with | Some nested_values, _ when not (Vec.is_empty nested_values) -> map_value (Vec.peek_front nested_values) | _, Some nested_values when not (Vec.is_empty nested_values) -> map_value (Vec.peek_front nested_values) | _ -> None)) | _, _, Some _ -> Some value | _ -> None let id_of_entity value = Edn_util.get_int64 value "db/id" let ident_of_entity value = Option.bind (Edn_util.get value "db/ident") Edn_util.as_keyword_t 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 "logseq.class/Tag" -> true | _ -> false) tags | None -> false let tag_id_of_result result = match first_entity result with | Some entity when tag_entity entity -> ( match id_of_entity entity with | Some id -> Ok id | None -> Error (Error.make Error.Tag_not_found "tag not found")) | _ -> Error (Error.make Error.Tag_not_found "tag not found") let property_entity value = Option.is_some (Edn_util.get value "logseq.property/type") let property_public value = match Edn_util.get_bool value "logseq.property/public?" with | Some false -> false | _ -> true let property_ident_of_entity entity = if property_entity entity && property_public entity then match ident_of_entity entity with | Some ident -> Ok ident | None -> Error (Error.make Error.Property_not_found "property not found") else Error (Error.make Error.Property_not_found "property not found") let property_ident_of_query result = match first_entity result with | Some entity -> property_ident_of_entity entity | None -> Error (Error.make Error.Property_not_found "property not found") let keyword_label ident = let value = Edn_util.keyword_to_string ident in if String.length value > 0 && value.[0] = ':' then String.sub value 1 (String.length value - 1) else value let qualified_keyword ident = String.contains (keyword_label ident) '/' let parse_tag_selector value = let value = String.trim value in match Int64.of_string_opt value with | Some id -> Selector.Tag_id id | None -> if Cli_primitive.is_uuid_string value then Selector.Tag_uuid value else if String.length value > 0 && value.[0] = ':' then Selector.Tag_ident (Edn_util.keyword_t value) else Selector.Tag_name value let parse_property_selector value = let value = String.trim value in match Int64.of_string_opt value with | Some id -> Property.Key_id id | None -> if String.length value > 0 && value.[0] = ':' then Property.Key_ident (Edn_util.keyword_t value) else Key_name value let resolve_tag_id invoke_config repo selector = let open Cli_effect in match selector with | Selector.Tag_id id -> pure (Ok id) | Tag_name name -> bind (pull_tag_by_name invoke_config repo name) (fun result -> pure (tag_id_of_result result)) | Tag_ident ident -> bind (pull_tag_by_name invoke_config repo (Edn_util.keyword_to_string ident)) (fun result -> pure (tag_id_of_result result)) | Tag_uuid uuid -> bind (pull_tag_by_uuid invoke_config repo uuid) (fun result -> pure (tag_id_of_result result)) let resolve_tag_ids invoke_config repo selectors = let open Cli_effect in let rec loop acc remaining = match Vec.pop_front remaining with | None -> pure (Ok acc) | Some (selector, rest) -> bind (resolve_tag_id invoke_config repo selector) (function | Error err -> pure (Error err) | Ok id -> loop (Vec.push_back acc id) rest) in loop Vec.empty selectors let resolve_property_ident invoke_config repo selector = let open Cli_effect in match selector with | Property.Key_ident ident -> bind (pull_property_by_ident invoke_config repo (Edn_util.keyword_to_string ident)) (fun entity -> match property_ident_of_entity entity with | Ok _ as result -> pure result | Error _ when not (qualified_keyword ident) -> bind (pull_property_by_name invoke_config repo (keyword_label ident)) (fun result -> pure (property_ident_of_query result)) | Error _ as result -> pure result) | Key_id id -> bind (pull_property_by_id invoke_config repo id) (fun entity -> pure (property_ident_of_entity entity)) | Key_name name -> bind (pull_property_by_name invoke_config repo name) (fun result -> match property_ident_of_query result with | Ok _ as result -> pure result | Error _ -> bind (pull_property_by_ident invoke_config repo (Edn_util.keyword_to_string (Edn_util.keyword_t name))) (fun entity -> pure (property_ident_of_entity entity))) let resolve_property_idents invoke_config repo selectors = let open Cli_effect in let rec loop acc remaining = match Vec.pop_front remaining with | None -> pure (Ok acc) | Some (selector, rest) -> bind (resolve_property_ident invoke_config repo selector) (function | Error err -> pure (Error err) | Ok ident -> loop (Vec.push_back acc ident) rest) in loop Vec.empty selectors let normalize_task_options invoke_config repo options = let open Cli_effect in let options = match Edn_util.get_string options "priority" with | Some priority_input -> ( match normalize_priority priority_input with | Some priority -> Edn_util.assoc "priority" (Edn_util.keyword priority) options | None -> options) | None -> options in match Edn_util.get_string options "status" with | None -> pure (Ok options) | Some status_input when String.trim status_input = "" -> pure (Ok options) | Some status_input -> bind (status_query invoke_config repo) (fun result -> let values = match (Edn_util.as_vector result, Edn_util.as_list result) with | Some values, _ | _, Some values -> values | _ -> 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 (Edn_util.assoc "status" (Edn_util.any ident) options)) | None -> pure (Error (Error.invalid_options (Task_status.invalid_status_message status_input statuses)))) let bool_option options key = match Edn_util.get_bool options key with | Some true -> true | Some false | None -> false let fields_include options field = match (common_of_options options).fields with | Some fields -> Vec.exists (( = ) field) fields | None -> false let normalize_tag_options options = if bool_option options "with-properties" || bool_option options "with-extends" then Edn_util.assoc "expand" (Edn_util.bool true) options else options let normalize_property_options options = if bool_option options "with-classes" then Edn_util.assoc "expand" (Edn_util.bool true) options else options let normalize_node_options invoke_config repo options = let open Cli_effect in let tag_selectors = Edn_util.get options "tags" |> Option.map string_list_of_value |> Option.value ~default:Vec.empty |> Vec.map parse_tag_selector in let property_selectors = Edn_util.get options "properties" |> Option.map string_list_of_value |> Option.value ~default:Vec.empty |> Vec.map parse_property_selector in bind (resolve_tag_ids invoke_config repo tag_selectors) (function | Error err -> pure (Error err) | Ok tag_ids -> bind (resolve_property_idents invoke_config repo property_selectors) (function | Error err -> pure (Error err) | Ok property_idents -> let options = options |> Edn_util.remove "tags" |> Edn_util.remove "properties" in let options = if Vec.is_empty tag_ids then options else Edn_util.assoc "tag-ids" (Edn_util.vector_vec (tag_ids |> Vec.map (fun id -> Edn_util.int64 id))) options in let options = if Vec.is_empty property_idents then options else Edn_util.assoc "property-idents" (Edn_util.vector_vec (Vec.map (fun ident -> Edn_util.any ident) property_idents)) options in pure (Ok options))) let normalize_asset_options invoke_config repo options = let open Cli_effect in bind (pull_asset_tag invoke_config repo) (fun entity -> match id_of_entity entity with | Some id -> pure (Ok (Edn_util.assoc "tag-ids" (Edn_util.vector_vec (Vec.of_array [| Edn_util.int64 id |])) options)) | None -> pure (Error (Error.make Error.Asset_tag_not_found "asset tag not found"))) let prepare_tag_item options item = ( ( item |> fun item -> if bool_option options "with-properties" then item else Edn_util.remove "logseq.property.class/properties" item ) |> fun item -> if bool_option options "with-extends" then item else Edn_util.remove "logseq.property.class/extends" item ) |> fun item -> if fields_include options "description" then item else Edn_util.remove "logseq.property/description" item let prepare_property_item options item = let with_type = match Edn_util.get_bool options "with-type" with | Some value -> value | None -> true in ( ( item |> fun item -> if bool_option options "with-classes" then item else Edn_util.remove "logseq.property/classes" item ) |> fun item -> if with_type then item else Edn_util.remove "logseq.property/type" item ) |> fun item -> if fields_include options "description" then item else Edn_util.remove "logseq.property/description" item let prepare_page_item item = Edn_util.remove "logseq.property/type" item let prepare_items kind options items = match kind with | Page -> Vec.map prepare_page_item items | Tag -> Vec.map (prepare_tag_item options) items | Property -> Vec.map (prepare_property_item options) items | Task | Node | Asset -> items let visible_title_fields = function | Node -> Vec.of_array [| Edn_util.keyword_t "block/title"; Edn_util.keyword_t "block/page-title"; |] | Page | Tag | Property | Task | Asset -> Vec.singleton (Edn_util.keyword_t "block/title") let normalize_visible_title_fields config repo kind items = let fields = visible_title_fields kind in let entities = Vec.map Entity.of_value items in let uuids = Uuid_refs_types.collect_uuid_refs_from_items entities fields in if Vec.is_empty uuids then Cli_effect.pure items else Cli_effect.map (fun labels -> Uuid_refs_types.normalize_item_string_fields entities fields labels |> Vec.map (fun item -> item.Entity.raw)) (Uuid_refs_types.fetch_uuid_labels config repo uuids) let add_optional key value fields = match value with | None -> fields | Some value -> Vec.push_back fields (Edn_util.keyword key, value) let common_options common = Vec.empty |> add_optional "fields" (Option.map value_of_string_list common.fields) |> add_optional "limit" (Option.map (fun value -> Edn_util.int value) common.limit) |> add_optional "offset" (Option.map (fun value -> Edn_util.int value) common.offset) |> add_optional "sort" (Option.map (fun value -> Edn_util.string value) common.sort) |> add_optional "order" (Option.map (fun value -> Edn_util.string (string_of_order value)) common.order) let add_bool key enabled fields = if enabled then Vec.push_back fields (Edn_util.keyword key, Edn_util.bool true) else fields let add_optional_bool key value fields = add_optional key (Option.map (fun value -> Edn_util.bool value) value) fields let options_of_parsed = function | Parsed_page opts -> common_options opts.common |> add_bool "expand" opts.expand |> add_optional_bool "include-built-in" opts.include_built_in |> add_optional_bool "include-journal" opts.include_journal |> add_bool "journal-only" opts.journal_only |> add_bool "include-hidden" opts.include_hidden |> add_optional "updated-after" (Option.map (fun value -> Edn_util.int64 (Time.time_to_epoch_ms value)) opts.updated_after) |> add_optional "created-after" (Option.map (fun value -> Edn_util.int64 (Time.time_to_epoch_ms value)) opts.created_after) |> Edn_util.map_vec | Parsed_tag opts -> common_options opts.common |> add_bool "expand" opts.expand |> add_optional_bool "include-built-in" opts.include_built_in |> add_bool "with-properties" opts.with_properties |> add_bool "with-extends" opts.with_extends |> Edn_util.map_vec | Parsed_property opts -> common_options opts.common |> add_bool "expand" opts.expand |> add_optional_bool "include-built-in" opts.include_built_in |> add_bool "with-classes" opts.with_classes |> add_bool "with-type" opts.with_type |> Edn_util.map_vec | Parsed_task opts -> common_options opts.common |> add_optional "status" (Option.map (fun value -> Edn_util.string value) opts.status) |> add_optional "priority" (Option.map (fun value -> Edn_util.string value) opts.priority) |> add_optional "content" (Option.map (fun value -> Edn_util.string value) opts.content) |> Edn_util.map_vec | Parsed_node opts -> common_options opts.common |> add_optional "tags" (if Vec.is_empty opts.tags then None else Some (value_of_string_list opts.tags)) |> add_optional "properties" (if Vec.is_empty opts.properties then None else Some (value_of_string_list opts.properties)) |> Edn_util.map_vec | Parsed_asset opts -> Edn_util.map_vec (opts.common |> common_options) let build ?registry:_ config _globals parsed = Error.bind (normalize_options parsed) (fun parsed -> match config.Cli_config.repo with | None -> Error (Error.missing_repo "repo is required for list") | Some repo -> Ok { kind = kind_of_parsed parsed; command = command_id parsed; repo; graph = Cli_config.repo_to_graph repo; options = options_of_parsed parsed; }) let items_value items = Edn_util.map_t_vec (Vec.of_array [| (Edn_util.keyword "items", Edn_util.vector_vec items) |]) let execute_with_mode action config mode = let open Cli_effect in bind (Server_runtime.ensure_server config action.repo ~create_empty_db:false) (function | Error err -> pure (Output_mode.error ~command:action.command mode err) | Ok invoke_config -> let options = match action.kind with | Tag -> pure (Ok (normalize_tag_options action.options)) | Property -> pure (Ok (normalize_property_options action.options)) | Task -> normalize_task_options invoke_config action.repo action.options | Node -> normalize_node_options invoke_config action.repo action.options | Asset -> normalize_asset_options invoke_config action.repo action.options | _ -> pure (Ok action.options) in bind options (function | Error err -> pure (Output_mode.error ~command:action.command mode err) | Ok options -> let options = Edn_util.expect_map_t "list command options" options in bind (match action.kind with | Page -> Transport.thread_api_cli_list_pages invoke_config ~repo:action.repo ~options | Tag -> Transport.thread_api_cli_list_tags invoke_config ~repo:action.repo ~options | Property -> Transport.thread_api_cli_list_properties invoke_config ~repo:action.repo ~options | Task -> Transport.thread_api_cli_list_tasks invoke_config ~repo:action.repo ~options | Node | Asset -> Transport.thread_api_cli_list_nodes invoke_config ~repo:action.repo ~options) (fun value -> let items = match (Edn_util.as_vector value, Edn_util.as_list value) with | Some xs, _ | _, Some xs -> xs | _ when Edn_util.is_null value -> Vec.empty | _ -> Vec.singleton value in let items = prepare_items action.kind action.options items in let items = postprocess_items action.kind action.options items in bind (normalize_visible_title_fields config action.repo action.kind items) (fun items -> let data = Edn_util.any (items_value items) in pure (Cli_result.ok ~command:action.command mode (Raw data)))))) 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 list page --graph my-graph"; "logseq list page --graph my-graph --journal-only --limit 20"; "logseq list page --graph my-graph --limit 50 --sort updated-at \ --order desc"; |]) Command_id.List_page "List pages"; meta ~examples: (Vec.of_array [| "logseq list tag --graph my-graph --with-properties"; "logseq list tag --graph my-graph --include-built-in --limit 20 \ --output json"; |]) List_tag "List tags"; meta ~examples: (Vec.of_array [| "logseq list property --graph my-graph --with-type"; "logseq list property --graph my-graph --include-built-in \ --limit 20 --output json"; |]) List_property "List properties"; meta ~examples: (Vec.of_array [| "logseq list task --graph my-graph --status todo --priority high"; "logseq list task --graph my-graph --content \"release\" --sort \ updated-at --order desc"; |]) List_task "List tasks"; meta ~examples: (Vec.of_array [| "logseq list node --graph my-graph --tags project,work"; "logseq list node --graph my-graph --properties status,priority \ --sort updated-at --order desc"; |]) List_node "List nodes"; meta ~examples: (Vec.of_array [| "logseq list asset --graph my-graph"; "logseq list asset --graph my-graph --limit 20 --sort \ updated-at --order desc"; |]) List_asset "List assets"; |] let execute action config = let (Output.Mode.Packed mode) = Output_mode.for_config config in execute_with_mode action config mode