@@ -9,18 +9,28 @@ import (
99 "strconv"
1010 "strings"
1111 "text/tabwriter"
12+ "time"
1213
1314 "github.com/pepabo/xpoint-cli/internal/xpoint"
1415 "github.com/spf13/cobra"
1516)
1617
1718var (
18- docSearchBody string
19- docSearchSize int
20- docSearchOffset int
21- docSearchPage int
22- docSearchOutput string
23- docSearchJQ string
19+ docSearchBody string
20+ docSearchSize int
21+ docSearchOffset int
22+ docSearchPage int
23+ docSearchOutput string
24+ docSearchJQ string
25+ docSearchTitle string
26+ docSearchFormName string
27+ docSearchFormID int
28+ docSearchFGID int
29+ docSearchWriters []string
30+ docSearchGroups []string
31+ docSearchMe bool
32+ docSearchSince string
33+ docSearchUntil string
2434
2535 docCreateBody string
2636 docCreateOutput string
@@ -50,12 +60,27 @@ var documentSearchCmd = &cobra.Command{
5060 Short : "Search documents" ,
5161 Long : `Search documents via POST /api/v1/search/documents.
5262
53- The search condition JSON is provided with --body, which accepts one of:
63+ The search condition can be specified by either a raw JSON body (--body) or
64+ convenience filter flags. Mixing --body with filter flags is rejected.
65+
66+ Raw body (--body) accepts one of:
5467 - inline JSON string (e.g. --body '{"title":"経費"}')
5568 - a path to a JSON file (e.g. --body ./search.json)
5669 - "-" to read the body from stdin (e.g. --body -)
5770
58- If --body is omitted, an empty object is sent (matches all documents).` ,
71+ Filter flags build a search body automatically:
72+ --title <s> partial match on the document title (件名)
73+ --form-name <s> partial match on the form name
74+ --form-id <n> form ID (fid)
75+ --form-group-id <n> form group ID (fgid)
76+ --writer <code> writer user code (repeatable)
77+ --writer-group <code> writer user-group code (repeatable)
78+ --me shorthand for --writer <current user code>
79+ --since <YYYY-MM-DD> lower bound of 新規更新日 (cr_dt)
80+ --until <YYYY-MM-DD> upper bound of 新規更新日 (cr_dt)
81+
82+ If neither --body nor any filter flag is given, an empty object is sent
83+ (matches all documents).` ,
5984 RunE : runDocumentSearch ,
6085}
6186
@@ -136,12 +161,21 @@ func init() {
136161 documentCmd .AddCommand (documentDownloadCmd )
137162
138163 f := documentSearchCmd .Flags ()
139- f .StringVar (& docSearchBody , "body" , "" , "search condition JSON: inline, file path, or - for stdin" )
164+ f .StringVar (& docSearchBody , "body" , "" , "search condition JSON: inline, file path, or - for stdin (cannot be combined with filter flags) " )
140165 f .IntVar (& docSearchSize , "size" , 0 , "number of items per page (0 = omit, server default 50; max 1000)" )
141166 f .IntVar (& docSearchOffset , "offset" , 0 , "result offset (0 = omit)" )
142167 f .IntVar (& docSearchPage , "page" , 0 , "result page (0 = omit)" )
143168 f .StringVarP (& docSearchOutput , "output" , "o" , "" , "output format: table|json (default: table on TTY, json otherwise)" )
144169 f .StringVar (& docSearchJQ , "jq" , "" , "apply a gojq filter to the JSON response (forces JSON output)" )
170+ f .StringVar (& docSearchTitle , "title" , "" , "partial match on document title" )
171+ f .StringVar (& docSearchFormName , "form-name" , "" , "partial match on form name" )
172+ f .IntVar (& docSearchFormID , "form-id" , 0 , "form ID (fid); 0 = omit" )
173+ f .IntVar (& docSearchFGID , "form-group-id" , 0 , "form group ID (fgid); 0 = omit" )
174+ f .StringSliceVar (& docSearchWriters , "writer" , nil , "writer user code (repeatable)" )
175+ f .StringSliceVar (& docSearchGroups , "writer-group" , nil , "writer user-group code (repeatable)" )
176+ f .BoolVar (& docSearchMe , "me" , false , "restrict to documents written by the current user (XPOINT_USER)" )
177+ f .StringVar (& docSearchSince , "since" , "" , "lower bound of 新規更新日 (YYYY-MM-DD)" )
178+ f .StringVar (& docSearchUntil , "until" , "" , "upper bound of 新規更新日 (YYYY-MM-DD)" )
145179
146180 cf := documentCreateCmd .Flags ()
147181 cf .StringVar (& docCreateBody , "body" , "" , "request body JSON: inline, file path, or - for stdin (required)" )
@@ -177,6 +211,20 @@ func runDocumentSearch(cmd *cobra.Command, args []string) error {
177211 return err
178212 }
179213
214+ hasFilters := docSearchTitle != "" || docSearchFormName != "" || docSearchFormID != 0 ||
215+ docSearchFGID != 0 || len (docSearchWriters ) > 0 || len (docSearchGroups ) > 0 ||
216+ docSearchMe || docSearchSince != "" || docSearchUntil != ""
217+ if hasFilters {
218+ if len (bodyBytes ) > 0 {
219+ return fmt .Errorf ("--body cannot be combined with filter flags (--title, --form-*, --writer*, --me, --since, --until)" )
220+ }
221+ built , err := buildSearchBodyFromFlags ()
222+ if err != nil {
223+ return err
224+ }
225+ bodyBytes = built
226+ }
227+
180228 params := xpoint.SearchDocumentsParams {}
181229 if docSearchSize != 0 {
182230 v := docSearchSize
@@ -407,6 +455,85 @@ func confirmDelete(docID int) bool {
407455 return false
408456}
409457
458+ type writerListEntry struct {
459+ Type string `json:"type"`
460+ Code string `json:"code"`
461+ }
462+
463+ // buildSearchBodyFromFlags converts --title / --form-* / --writer* / --me /
464+ // --since / --until into a JSON request body for POST /api/v1/search/documents.
465+ func buildSearchBodyFromFlags () (json.RawMessage , error ) {
466+ body := map [string ]any {}
467+
468+ if docSearchTitle != "" {
469+ body ["title" ] = docSearchTitle
470+ }
471+ if docSearchFormName != "" {
472+ body ["form_name" ] = docSearchFormName
473+ }
474+ if docSearchFormID != 0 {
475+ body ["fid" ] = docSearchFormID
476+ }
477+ if docSearchFGID != 0 {
478+ body ["fgid" ] = docSearchFGID
479+ }
480+
481+ var writers []writerListEntry
482+ for _ , code := range docSearchWriters {
483+ if code = strings .TrimSpace (code ); code != "" {
484+ writers = append (writers , writerListEntry {Type : "user" , Code : code })
485+ }
486+ }
487+ for _ , code := range docSearchGroups {
488+ if code = strings .TrimSpace (code ); code != "" {
489+ writers = append (writers , writerListEntry {Type : "group" , Code : code })
490+ }
491+ }
492+ if docSearchMe {
493+ me := pick (flagUser , "XPOINT_USER" )
494+ if me == "" {
495+ return nil , fmt .Errorf ("--me requires the current user code: set --xpoint-user or XPOINT_USER" )
496+ }
497+ writers = append (writers , writerListEntry {Type : "user" , Code : me })
498+ }
499+ if len (writers ) > 0 {
500+ body ["writer_list" ] = writers
501+ }
502+
503+ if docSearchSince != "" || docSearchUntil != "" {
504+ body ["date_type" ] = "cr_dt"
505+ body ["dt_cond_type" ] = "1"
506+ if docSearchSince != "" {
507+ t , err := parseSearchDate (docSearchSince )
508+ if err != nil {
509+ return nil , fmt .Errorf ("--since: %w" , err )
510+ }
511+ body ["lower_year" ] = t .Year ()
512+ body ["lower_month" ] = int (t .Month ())
513+ body ["lower_day" ] = t .Day ()
514+ }
515+ if docSearchUntil != "" {
516+ t , err := parseSearchDate (docSearchUntil )
517+ if err != nil {
518+ return nil , fmt .Errorf ("--until: %w" , err )
519+ }
520+ body ["upper_year" ] = t .Year ()
521+ body ["upper_month" ] = int (t .Month ())
522+ body ["upper_day" ] = t .Day ()
523+ }
524+ }
525+
526+ return json .Marshal (body )
527+ }
528+
529+ func parseSearchDate (s string ) (time.Time , error ) {
530+ t , err := time .Parse ("2006-01-02" , strings .TrimSpace (s ))
531+ if err != nil {
532+ return time.Time {}, fmt .Errorf ("invalid date %q: expected YYYY-MM-DD" , s )
533+ }
534+ return t , nil
535+ }
536+
410537// loadSearchBody resolves --body into JSON bytes.
411538func loadSearchBody (spec string ) (json.RawMessage , error ) {
412539 if spec == "" {
0 commit comments