Skip to content

Commit 30cfe0e

Browse files
buty4649claude
andauthored
document: search にフィルタフラグを追加 (#19)
* 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> * document: --me で /scim/v2/{domain_code}/Me をフォールバック XPOINT_USER(もしくは --xpoint-user)が設定されていればそれを使い、未設定時は /scim/v2/{domain_code}/Me を呼んで認証ユーザの userName を取得するようにする。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * document: --me の domain_code を保存済みOAuthトークンからも解決 --xpoint-domain-code / XPOINT_DOMAIN_CODE が未設定でも `xp auth login` で キーリングに保存された domain_code をフォールバックとして使うように resolveDomainCode を追加。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * xpoint: SCIMエンドポイントのAcceptヘッダをapplication/scim+jsonに /scim/v2/{domain_code}/Me は application/json では 406 Not Acceptable を返すため、do を doAccept に分けて呼び出し側で Accept を指定可能にし、 GetSelfInfo は application/scim+json を送るようにする。 * document: --me のユーザコードを atled SCIM 拡張の userCode から取得 X-point の writer_list には userName(ログイン名、例: ykky)ではなく ユーザコード(例: 326)を渡す必要があり、従来実装では SCIM の userName を使っていたため writer_list が効かず結果が絞れていなかった。 SCIM レスポンスの urn:atled:scim:schemas:1.0:User.userCode を読み、 そちらを writer_list.code に送るようにする。 また --body 利用時のリクエストボディをデバッグ出力に追加し、調査時に 送信内容が確認できるようにする。 * me: 認証ユーザー情報を表示するサブコマンドを追加 xp me で GET /scim/v2/{domain_code}/Me を呼び、ログイン名 (userName)・表示名・X-point ユーザコード (atled 拡張 userCode) を テーブル/JSON で表示する。writer_list に渡すべきユーザコードを 手元で確認したいケースを想定。 * cmd/document: gofmt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 9d29113 commit 30cfe0e

7 files changed

Lines changed: 513 additions & 10 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,17 @@ cat search.json | xp document search --body -
109109
xp document search --size 100 --page 2
110110
```
111111

112+
フィルタフラグで簡易検索もできます(`--body` とは併用不可)。
113+
114+
```sh
115+
xp document search --title 経費 # 件名部分一致
116+
xp document search --form-name 稟議 --form-group-id 3 # フォーム名 + フォームグループID
117+
xp document search --writer alice --writer bob # 申請者指定(複数可)
118+
xp document search --writer-group grp1 # 申請者グループ指定
119+
xp document search --me # 自分が申請者の書類(XPOINT_USER、未設定なら /scim/v2/{domain_code}/Me の atled 拡張 userCode を利用。domain_code は保存済み OAuth トークンの値も利用)
120+
xp document search --since 2024-01-01 --until 2024-12-31
121+
```
122+
112123
### ドキュメントの承認状況取得
113124

114125
```sh
@@ -145,6 +156,17 @@ xp document download 266248 -o pdfs/ # 指定ディレクトリにサー
145156
xp document download 266248 -o - > out.pdf # 標準出力に書き出し
146157
```
147158

159+
### 認証ユーザー情報の確認
160+
161+
```sh
162+
xp me # GET /scim/v2/{domain_code}/Me の結果を表示
163+
xp me --jq .userName
164+
```
165+
166+
OAuth 認証済みであることが前提です(汎用APIトークンでは SCIM は使えません)。`domain_code``--xpoint-domain-code` / `XPOINT_DOMAIN_CODE` / 保存済み OAuth トークンの順で解決されます。
167+
168+
`user_code` は X-point の内部ユーザコード(例: `326`)で、`document search --writer` などの writer_list API で使う値です。`user_name` は SCIM の `userName`(ログイン名、例: `ykky`)です。
169+
148170
### レスポンススキーマの確認
149171

150172
```sh

cmd/document.go

Lines changed: 167 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"io"
@@ -9,18 +10,28 @@ import (
910
"strconv"
1011
"strings"
1112
"text/tabwriter"
13+
"time"
1214

1315
"github.com/pepabo/xpoint-cli/internal/xpoint"
1416
"github.com/spf13/cobra"
1517
)
1618

1719
var (
18-
docSearchBody string
19-
docSearchSize int
20-
docSearchOffset int
21-
docSearchPage int
22-
docSearchOutput string
23-
docSearchJQ string
20+
docSearchBody string
21+
docSearchSize int
22+
docSearchOffset int
23+
docSearchPage int
24+
docSearchOutput string
25+
docSearchJQ string
26+
docSearchTitle string
27+
docSearchFormName string
28+
docSearchFormID int
29+
docSearchFGID int
30+
docSearchWriters []string
31+
docSearchGroups []string
32+
docSearchMe bool
33+
docSearchSince string
34+
docSearchUntil string
2435

2536
docCreateBody string
2637
docCreateOutput string
@@ -69,12 +80,28 @@ var documentSearchCmd = &cobra.Command{
6980
Short: "Search documents",
7081
Long: `Search documents via POST /api/v1/search/documents.
7182
72-
The search condition JSON is provided with --body, which accepts one of:
83+
The search condition can be specified by either a raw JSON body (--body) or
84+
convenience filter flags. Mixing --body with filter flags is rejected.
85+
86+
Raw body (--body) accepts one of:
7387
- inline JSON string (e.g. --body '{"title":"経費"}')
7488
- a path to a JSON file (e.g. --body ./search.json)
7589
- "-" to read the body from stdin (e.g. --body -)
7690
77-
If --body is omitted, an empty object is sent (matches all documents).`,
91+
Filter flags build a search body automatically:
92+
--title <s> partial match on the document title (件名)
93+
--form-name <s> partial match on the form name
94+
--form-id <n> form ID (fid)
95+
--form-group-id <n> form group ID (fgid)
96+
--writer <code> writer user code (repeatable)
97+
--writer-group <code> writer user-group code (repeatable)
98+
--me shorthand for --writer <current user code>;
99+
looked up via XPOINT_USER or /scim/v2/{domain_code}/Me
100+
--since <YYYY-MM-DD> lower bound of 新規更新日 (cr_dt)
101+
--until <YYYY-MM-DD> upper bound of 新規更新日 (cr_dt)
102+
103+
If neither --body nor any filter flag is given, an empty object is sent
104+
(matches all documents).`,
78105
RunE: runDocumentSearch,
79106
}
80107

@@ -230,12 +257,21 @@ func init() {
230257
documentCommentCmd.AddCommand(documentCommentDeleteCmd)
231258

232259
f := documentSearchCmd.Flags()
233-
f.StringVar(&docSearchBody, "body", "", "search condition JSON: inline, file path, or - for stdin")
260+
f.StringVar(&docSearchBody, "body", "", "search condition JSON: inline, file path, or - for stdin (cannot be combined with filter flags)")
234261
f.IntVar(&docSearchSize, "size", 0, "number of items per page (0 = omit, server default 50; max 1000)")
235262
f.IntVar(&docSearchOffset, "offset", 0, "result offset (0 = omit)")
236263
f.IntVar(&docSearchPage, "page", 0, "result page (0 = omit)")
237264
f.StringVarP(&docSearchOutput, "output", "o", "", "output format: table|json (default: table on TTY, json otherwise)")
238265
f.StringVar(&docSearchJQ, "jq", "", "apply a gojq filter to the JSON response (forces JSON output)")
266+
f.StringVar(&docSearchTitle, "title", "", "partial match on document title")
267+
f.StringVar(&docSearchFormName, "form-name", "", "partial match on form name")
268+
f.IntVar(&docSearchFormID, "form-id", 0, "form ID (fid); 0 = omit")
269+
f.IntVar(&docSearchFGID, "form-group-id", 0, "form group ID (fgid); 0 = omit")
270+
f.StringSliceVar(&docSearchWriters, "writer", nil, "writer user code (repeatable)")
271+
f.StringSliceVar(&docSearchGroups, "writer-group", nil, "writer user-group code (repeatable)")
272+
f.BoolVar(&docSearchMe, "me", false, "restrict to documents written by the current user (XPOINT_USER, or /scim/v2/{domain_code}/Me)")
273+
f.StringVar(&docSearchSince, "since", "", "lower bound of 新規更新日 (YYYY-MM-DD)")
274+
f.StringVar(&docSearchUntil, "until", "", "upper bound of 新規更新日 (YYYY-MM-DD)")
239275

240276
cf := documentCreateCmd.Flags()
241277
cf.StringVar(&docCreateBody, "body", "", "request body JSON: inline, file path, or - for stdin (required)")
@@ -299,6 +335,27 @@ func runDocumentSearch(cmd *cobra.Command, args []string) error {
299335
return err
300336
}
301337

338+
hasFilters := docSearchTitle != "" || docSearchFormName != "" || docSearchFormID != 0 ||
339+
docSearchFGID != 0 || len(docSearchWriters) > 0 || len(docSearchGroups) > 0 ||
340+
docSearchMe || docSearchSince != "" || docSearchUntil != ""
341+
if hasFilters {
342+
if len(bodyBytes) > 0 {
343+
return fmt.Errorf("--body cannot be combined with filter flags (--title, --form-*, --writer*, --me, --since, --until)")
344+
}
345+
meCode := ""
346+
if docSearchMe {
347+
meCode, err = resolveCurrentUserCode(cmd.Context(), client)
348+
if err != nil {
349+
return err
350+
}
351+
}
352+
built, err := buildSearchBodyFromFlags(meCode)
353+
if err != nil {
354+
return err
355+
}
356+
bodyBytes = built
357+
}
358+
302359
params := xpoint.SearchDocumentsParams{}
303360
if docSearchSize != 0 {
304361
v := docSearchSize
@@ -721,6 +778,107 @@ func confirmDelete(docID int) bool {
721778
return false
722779
}
723780

781+
type writerListEntry struct {
782+
Type string `json:"type"`
783+
Code string `json:"code"`
784+
}
785+
786+
// resolveCurrentUserCode returns the authenticated user's X-point user code
787+
// for --me. It prefers XPOINT_USER / --xpoint-user; if neither is set, it
788+
// falls back to GET /scim/v2/{domain_code}/Me and reads the atled SCIM
789+
// extension's userCode (not userName — that's the login name, while the
790+
// writer_list API expects the numeric user code).
791+
func resolveCurrentUserCode(ctx context.Context, client *xpoint.Client) (string, error) {
792+
if u := pick(flagUser, "XPOINT_USER"); u != "" {
793+
return u, nil
794+
}
795+
domain := resolveDomainCode()
796+
if domain == "" {
797+
return "", fmt.Errorf("--me requires the current user code: set --xpoint-user / XPOINT_USER, or provide a domain code (--xpoint-domain-code / XPOINT_DOMAIN_CODE / stored OAuth login) to look it up via /scim/v2/{domain_code}/Me")
798+
}
799+
info, err := client.GetSelfInfo(ctx, domain)
800+
if err != nil {
801+
return "", fmt.Errorf("resolve --me via /scim/v2/%s/Me: %w", domain, err)
802+
}
803+
if info.AtledExt.UserCode == "" {
804+
return "", fmt.Errorf("resolve --me: userCode is empty in /scim/v2/%s/Me response (atled SCIM extension missing)", domain)
805+
}
806+
return info.AtledExt.UserCode, nil
807+
}
808+
809+
// buildSearchBodyFromFlags converts --title / --form-* / --writer* / --me /
810+
// --since / --until into a JSON request body for POST /api/v1/search/documents.
811+
//
812+
// meCode is the resolved user code to use for --me (empty if --me was not set
813+
// or resolution is not needed).
814+
func buildSearchBodyFromFlags(meCode string) (json.RawMessage, error) {
815+
body := map[string]any{}
816+
817+
if docSearchTitle != "" {
818+
body["title"] = docSearchTitle
819+
}
820+
if docSearchFormName != "" {
821+
body["form_name"] = docSearchFormName
822+
}
823+
if docSearchFormID != 0 {
824+
body["fid"] = docSearchFormID
825+
}
826+
if docSearchFGID != 0 {
827+
body["fgid"] = docSearchFGID
828+
}
829+
830+
var writers []writerListEntry
831+
for _, code := range docSearchWriters {
832+
if code = strings.TrimSpace(code); code != "" {
833+
writers = append(writers, writerListEntry{Type: "user", Code: code})
834+
}
835+
}
836+
for _, code := range docSearchGroups {
837+
if code = strings.TrimSpace(code); code != "" {
838+
writers = append(writers, writerListEntry{Type: "group", Code: code})
839+
}
840+
}
841+
if meCode != "" {
842+
writers = append(writers, writerListEntry{Type: "user", Code: meCode})
843+
}
844+
if len(writers) > 0 {
845+
body["writer_list"] = writers
846+
}
847+
848+
if docSearchSince != "" || docSearchUntil != "" {
849+
body["date_type"] = "cr_dt"
850+
body["dt_cond_type"] = "1"
851+
if docSearchSince != "" {
852+
t, err := parseSearchDate(docSearchSince)
853+
if err != nil {
854+
return nil, fmt.Errorf("--since: %w", err)
855+
}
856+
body["lower_year"] = t.Year()
857+
body["lower_month"] = int(t.Month())
858+
body["lower_day"] = t.Day()
859+
}
860+
if docSearchUntil != "" {
861+
t, err := parseSearchDate(docSearchUntil)
862+
if err != nil {
863+
return nil, fmt.Errorf("--until: %w", err)
864+
}
865+
body["upper_year"] = t.Year()
866+
body["upper_month"] = int(t.Month())
867+
body["upper_day"] = t.Day()
868+
}
869+
}
870+
871+
return json.Marshal(body)
872+
}
873+
874+
func parseSearchDate(s string) (time.Time, error) {
875+
t, err := time.Parse("2006-01-02", strings.TrimSpace(s))
876+
if err != nil {
877+
return time.Time{}, fmt.Errorf("invalid date %q: expected YYYY-MM-DD", s)
878+
}
879+
return t, nil
880+
}
881+
724882
// loadSearchBody resolves --body into JSON bytes.
725883
func loadSearchBody(spec string) (json.RawMessage, error) {
726884
if spec == "" {

0 commit comments

Comments
 (0)