Skip to content

Commit 02abf13

Browse files
buty4649claude
andauthored
document: 承認状況取得サブコマンドを追加 (#20)
GET /api/v1/documents/{docid}/status を呼び出す `xp document status` サブコマンドを追加。--history で全バージョンの承認履歴取得、 --jq で gojq フィルタに対応。 Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 1745eaf commit 02abf13

7 files changed

Lines changed: 223 additions & 0 deletions

File tree

README.md

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

100+
### ドキュメントの承認状況取得
101+
102+
```sh
103+
xp document status 266248 # 最新版の承認状況(JSON)
104+
xp document status 266248 --history # 全バージョンの承認履歴も含める
105+
xp document status 266248 --jq '.document.status.name'
106+
```
107+
100108
### ドキュメントのPDFダウンロード
101109

102110
```sh

cmd/document.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ var (
3838
docDeleteJQ string
3939

4040
docDownloadOutput string
41+
42+
docStatusHistory bool
43+
docStatusJQ string
4144
)
4245

4346
var documentCmd = &cobra.Command{
@@ -112,6 +115,18 @@ By default the command prompts for confirmation. Pass --yes to skip it.`,
112115
RunE: runDocumentDelete,
113116
}
114117

118+
var documentStatusCmd = &cobra.Command{
119+
Use: "status <docid>",
120+
Short: "Get document approval status",
121+
Long: `Retrieve the approval status of a document via GET /api/v1/documents/{docid}/status.
122+
123+
The response is returned as JSON and contains the current status, step,
124+
writer, last approver, and the approval flow. Pass --history to include
125+
approval histories for all past versions.`,
126+
Args: cobra.ExactArgs(1),
127+
RunE: runDocumentStatus,
128+
}
129+
115130
var documentDownloadCmd = &cobra.Command{
116131
Use: "download <docid>",
117132
Short: "Download a document as PDF",
@@ -133,6 +148,7 @@ func init() {
133148
documentCmd.AddCommand(documentGetCmd)
134149
documentCmd.AddCommand(documentEditCmd)
135150
documentCmd.AddCommand(documentDeleteCmd)
151+
documentCmd.AddCommand(documentStatusCmd)
136152
documentCmd.AddCommand(documentDownloadCmd)
137153

138154
f := documentSearchCmd.Flags()
@@ -162,6 +178,10 @@ func init() {
162178
df.StringVarP(&docDeleteOutput, "output", "o", "", "output format: table|json (default: table on TTY, json otherwise)")
163179
df.StringVar(&docDeleteJQ, "jq", "", "apply a gojq filter to the JSON response (forces JSON output)")
164180

181+
sf := documentStatusCmd.Flags()
182+
sf.BoolVar(&docStatusHistory, "history", false, "include approval histories for all versions")
183+
sf.StringVar(&docStatusJQ, "jq", "", "apply a gojq filter to the JSON response")
184+
165185
dlf := documentDownloadCmd.Flags()
166186
dlf.StringVarP(&docDownloadOutput, "output", "o", "", "output path: FILE, DIR/, or - for stdout (default: server-provided filename in current directory)")
167187
}
@@ -333,6 +353,25 @@ func runDocumentDelete(cmd *cobra.Command, args []string) error {
333353
})
334354
}
335355

356+
func runDocumentStatus(cmd *cobra.Command, args []string) error {
357+
docID, err := parseDocID(args[0])
358+
if err != nil {
359+
return err
360+
}
361+
client, err := newClientFromFlags(cmd.Context())
362+
if err != nil {
363+
return err
364+
}
365+
raw, err := client.GetDocumentStatus(cmd.Context(), docID, docStatusHistory)
366+
if err != nil {
367+
return err
368+
}
369+
if docStatusJQ != "" {
370+
return runJQ(raw, docStatusJQ)
371+
}
372+
return writeJSON(os.Stdout, raw)
373+
}
374+
336375
func runDocumentDownload(cmd *cobra.Command, args []string) error {
337376
docID, err := parseDocID(args[0])
338377
if err != nil {

cmd/schema.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Supported aliases map to the CLI's commands:
2525
document.update PATCH /api/v1/documents/{docid}
2626
document.delete DELETE /api/v1/documents/{docid}
2727
document.download GET /api/v1/documents/{docid}/pdf
28+
document.status GET /api/v1/documents/{docid}/status
2829
2930
Run without arguments to list supported aliases.`,
3031
Args: cobra.MaximumNArgs(1),
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"method": "GET",
3+
"path": "/api/v1/documents/{docid}/status",
4+
"summary": "書類承認状況取得",
5+
"description": "指定された書類 ID の最新版の承認状況を取得する。閲覧権限が必要。\n",
6+
"parameters": [
7+
{
8+
"name": "docid",
9+
"in": "path",
10+
"type": "integer",
11+
"required": true,
12+
"description": "書類ID"
13+
},
14+
{
15+
"name": "history",
16+
"in": "query",
17+
"type": "boolean",
18+
"required": false,
19+
"description": "true の場合、全バージョンの承認履歴情報を含めて返す。既定は false。"
20+
}
21+
],
22+
"response": {
23+
"type": "object",
24+
"properties": {
25+
"document": {
26+
"type": "object",
27+
"description": "書類情報",
28+
"properties": {
29+
"docid": {
30+
"type": "integer",
31+
"description": "書類ID"
32+
},
33+
"title1": {
34+
"type": "string",
35+
"description": "件名1"
36+
},
37+
"title2": {
38+
"type": "string",
39+
"description": "件名2"
40+
},
41+
"form": {
42+
"type": "object",
43+
"description": "フォーム情報 (id, code, name)"
44+
},
45+
"route": {
46+
"type": "object",
47+
"description": "承認ルート情報 (code, name)"
48+
},
49+
"type": {
50+
"type": "string",
51+
"description": "フォーム種別 (standard: 通常フォーム / workflow: ワークフローフォーム)"
52+
},
53+
"status": {
54+
"type": "object",
55+
"description": "書類状態 (code: 0=下書き, 1=承認中, 2=保留中, 3=却下, 4=差し戻され, 5=差し戻し, 6=承認完了)"
56+
},
57+
"step": {
58+
"type": "object",
59+
"description": "ステップ情報 (max, current)"
60+
},
61+
"current_version": {
62+
"type": "integer",
63+
"description": "現在バージョン番号"
64+
},
65+
"writer": {
66+
"type": "object",
67+
"description": "申請者情報 (usercode, username, stampname, datetime)"
68+
},
69+
"lastaprv": {
70+
"type": "object",
71+
"description": "最終承認者情報 (usercode, username, stampname, datetime)"
72+
},
73+
"flow_versions": {
74+
"type": "array",
75+
"description": "最新バージョンの承認フロー結果。通常フォームの場合は空配列。"
76+
},
77+
"histories": {
78+
"type": "array",
79+
"description": "承認履歴情報 (version ごと)。クエリパラメータ history が true の場合に返る。通常フォームの場合は空配列。"
80+
}
81+
}
82+
}
83+
}
84+
}
85+
}

internal/schema/schema_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func TestAliases_Sorted(t *testing.T) {
1414
"document.download",
1515
"document.get",
1616
"document.search",
17+
"document.status",
1718
"document.update",
1819
"form.list",
1920
"form.show",
@@ -140,6 +141,31 @@ func TestLookup_DocumentDownload(t *testing.T) {
140141
}
141142
}
142143

144+
func TestLookup_DocumentStatus(t *testing.T) {
145+
op, err := Lookup("document.status")
146+
if err != nil {
147+
t.Fatalf("Lookup: %v", err)
148+
}
149+
if op["method"] != "GET" {
150+
t.Errorf("method = %v", op["method"])
151+
}
152+
if op["path"] != "/api/v1/documents/{docid}/status" {
153+
t.Errorf("path = %v", op["path"])
154+
}
155+
params, _ := op["parameters"].([]any)
156+
if len(params) != 2 {
157+
t.Fatalf("parameters = %v", params)
158+
}
159+
docid, _ := params[0].(map[string]any)
160+
if docid["name"] != "docid" || docid["required"] != true {
161+
t.Errorf("docid param = %v", docid)
162+
}
163+
history, _ := params[1].(map[string]any)
164+
if history["name"] != "history" || history["in"] != "query" {
165+
t.Errorf("history param = %v", history)
166+
}
167+
}
168+
143169
func TestLookup_DocumentDelete(t *testing.T) {
144170
op, err := Lookup("document.delete")
145171
if err != nil {

internal/xpoint/client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,24 @@ func (c *Client) DeleteDocument(ctx context.Context, docID int) (*DeleteDocument
355355
return &out, nil
356356
}
357357

358+
// GetDocumentStatus calls GET /api/v1/documents/{docid}/status.
359+
// When history is true, approval histories for every version are included.
360+
// The response shape is complex (form, status, step, flow_results, etc.),
361+
// so it is returned as raw JSON for the caller to interpret.
362+
func (c *Client) GetDocumentStatus(ctx context.Context, docID int, history bool) (json.RawMessage, error) {
363+
path := fmt.Sprintf("/api/v1/documents/%d/status", docID)
364+
var q url.Values
365+
if history {
366+
q = url.Values{}
367+
q.Set("history", "true")
368+
}
369+
var out json.RawMessage
370+
if err := c.do(ctx, http.MethodGet, path, q, nil, &out); err != nil {
371+
return nil, err
372+
}
373+
return out, nil
374+
}
375+
358376
// DownloadPDF calls GET /api/v1/documents/{docid}/pdf and returns the PDF
359377
// bytes and the server-provided filename (parsed from Content-Disposition,
360378
// which may use RFC 5987 encoding). The filename is empty when the server

internal/xpoint/client_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,52 @@ func TestDocumentURL(t *testing.T) {
356356
}
357357
}
358358

359+
func TestGetDocumentStatus(t *testing.T) {
360+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
361+
if r.Method != http.MethodGet {
362+
t.Errorf("method = %s, want GET", r.Method)
363+
}
364+
if r.URL.Path != "/api/v1/documents/999/status" {
365+
t.Errorf("path = %s", r.URL.Path)
366+
}
367+
if r.URL.Query().Has("history") {
368+
t.Errorf("history query should be omitted, got %q", r.URL.Query().Get("history"))
369+
}
370+
w.Header().Set("Content-Type", "application/json")
371+
_, _ = w.Write([]byte(`{"document":{"docid":999,"status":{"code":1,"name":"承認中"}}}`))
372+
}))
373+
defer srv.Close()
374+
375+
c := clientForServer(srv)
376+
raw, err := c.GetDocumentStatus(context.Background(), 999, false)
377+
if err != nil {
378+
t.Fatalf("GetDocumentStatus: %v", err)
379+
}
380+
var decoded map[string]any
381+
if err := json.Unmarshal(raw, &decoded); err != nil {
382+
t.Fatalf("raw not JSON: %v (%s)", err, string(raw))
383+
}
384+
doc, _ := decoded["document"].(map[string]any)
385+
if doc["docid"].(float64) != 999 {
386+
t.Errorf("docid = %v", doc["docid"])
387+
}
388+
}
389+
390+
func TestGetDocumentStatus_WithHistory(t *testing.T) {
391+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
392+
if got := r.URL.Query().Get("history"); got != "true" {
393+
t.Errorf("history = %q, want true", got)
394+
}
395+
_, _ = w.Write([]byte(`{"document":{"docid":1}}`))
396+
}))
397+
defer srv.Close()
398+
399+
c := clientForServer(srv)
400+
if _, err := c.GetDocumentStatus(context.Background(), 1, true); err != nil {
401+
t.Fatalf("GetDocumentStatus: %v", err)
402+
}
403+
}
404+
359405
func TestDownloadPDF(t *testing.T) {
360406
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
361407
if r.Method != http.MethodGet {

0 commit comments

Comments
 (0)