Skip to content

Commit 39a1798

Browse files
buty4649claude
andcommitted
document: search にフィルタフラグを追加
--title / --form-name / --form-id / --form-group-id / --writer / --writer-group / --me / --since / --until を追加し、--body を書かずに 簡易検索できるようにする。--body とフィルタフラグの併用はエラー。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 1745eaf commit 39a1798

3 files changed

Lines changed: 329 additions & 9 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,17 @@ cat search.json | xp document search --body -
9797
xp document search --size 100 --page 2
9898
```
9999

100+
フィルタフラグで簡易検索もできます(`--body` とは併用不可)。
101+
102+
```sh
103+
xp document search --title 経費 # 件名部分一致
104+
xp document search --form-name 稟議 --form-group-id 3 # フォーム名 + フォームグループID
105+
xp document search --writer alice --writer bob # 申請者指定(複数可)
106+
xp document search --writer-group grp1 # 申請者グループ指定
107+
xp document search --me # 自分が申請者の書類
108+
xp document search --since 2024-01-01 --until 2024-12-31
109+
```
110+
100111
### ドキュメントのPDFダウンロード
101112

102113
```sh

cmd/document.go

Lines changed: 136 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1718
var (
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.
411538
func loadSearchBody(spec string) (json.RawMessage, error) {
412539
if spec == "" {

0 commit comments

Comments
 (0)