Skip to content

Commit ee568f8

Browse files
buty4649claude
andauthored
query: 一覧取得・実行サブコマンドを追加 (#22)
- xp query list: 利用可能なクエリを一覧取得 (GET /api/v1/query/) - xp query exec <query_code>: クエリを実行して定義と結果を取得 (GET /api/v1/query/{query_code}) - --no-run で定義のみ、--rows/--offset で一覧クエリのページング - schema query.list / query.exec を追加 Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c7b727a commit ee568f8

File tree

8 files changed

+466
-0
lines changed

8 files changed

+466
-0
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ xp approval list --filter 'cr_dt between "2023-01-01" and "2023-12-31"'
8888

8989
`--stat` の値は X-point のマニュアル参照(10=承認待ち、20=通知、30=下書き等、40=状況確認、50=承認完了)。
9090

91+
### クエリ一覧 / 実行
92+
93+
```sh
94+
xp query list # 利用可能なクエリを一覧表示
95+
xp query list --jq '.query_groups[].queries[].query_code'
96+
97+
xp query exec query01 # クエリを実行して定義と結果を取得
98+
xp query exec query01 --no-run # 定義のみ取得(実行しない)
99+
xp query exec query01 --rows 100 --offset 0
100+
xp query exec query01 --jq '.exec_result.data'
101+
```
102+
91103
### ドキュメント検索
92104

93105
```sh

cmd/query.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
"text/tabwriter"
8+
9+
"github.com/pepabo/xpoint-cli/internal/xpoint"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var (
14+
queryListOutput string
15+
queryListJQ string
16+
17+
queryExecNoRun bool
18+
queryExecRows int
19+
queryExecOffset int
20+
queryExecJQ string
21+
)
22+
23+
var queryCmd = &cobra.Command{
24+
Use: "query",
25+
Short: "Manage X-point queries",
26+
}
27+
28+
var queryListCmd = &cobra.Command{
29+
Use: "list",
30+
Short: "List available queries",
31+
Long: "List available queries via GET /api/v1/query/.",
32+
RunE: runQueryList,
33+
}
34+
35+
var queryExecCmd = &cobra.Command{
36+
Use: "exec <query_code>",
37+
Short: "Execute a query and show its result",
38+
Long: `Fetch a query and its execution result via GET /api/v1/query/{query_code}.
39+
40+
By default the query is executed (exec_flg=true) and the response contains
41+
both the definition and exec_result. Pass --no-run to fetch the definition
42+
only. --rows (default 500) and --offset control pagination for list queries.`,
43+
Args: cobra.ExactArgs(1),
44+
RunE: runQueryExec,
45+
}
46+
47+
func init() {
48+
rootCmd.AddCommand(queryCmd)
49+
queryCmd.AddCommand(queryListCmd)
50+
queryCmd.AddCommand(queryExecCmd)
51+
52+
lf := queryListCmd.Flags()
53+
lf.StringVarP(&queryListOutput, "output", "o", "", "output format: table|json (default: table on TTY, json otherwise)")
54+
lf.StringVar(&queryListJQ, "jq", "", "apply a gojq filter to the JSON response (forces JSON output)")
55+
56+
ef := queryExecCmd.Flags()
57+
ef.BoolVar(&queryExecNoRun, "no-run", false, "do not execute the query; return the definition only (exec_flg=false)")
58+
ef.IntVar(&queryExecRows, "rows", 0, "max rows returned by list queries (0 = omit; server default 500; range 1-10000)")
59+
ef.IntVar(&queryExecOffset, "offset", 0, "offset for list queries (0 = omit)")
60+
ef.StringVar(&queryExecJQ, "jq", "", "apply a gojq filter to the JSON response")
61+
}
62+
63+
func runQueryList(cmd *cobra.Command, args []string) error {
64+
client, err := newClientFromFlags(cmd.Context())
65+
if err != nil {
66+
return err
67+
}
68+
res, err := client.ListAvailableQueries(cmd.Context())
69+
if err != nil {
70+
return err
71+
}
72+
return render(res, resolveOutputFormat(queryListOutput), queryListJQ, func() error {
73+
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
74+
defer w.Flush()
75+
fmt.Fprintln(w, "GROUP_ID\tGROUP_NAME\tQUERY_ID\tQUERY_CODE\tQUERY_NAME\tQUERY_TYPE\tFORM")
76+
for _, g := range res.QueryGroups {
77+
if len(g.Queries) == 0 {
78+
fmt.Fprintf(w, "%d\t%s\t-\t-\t-\t-\t-\n", g.QueryGroupID, g.QueryGroupName)
79+
continue
80+
}
81+
for _, q := range g.Queries {
82+
fmt.Fprintf(w, "%d\t%s\t%d\t%s\t%s\t%s\t%s\n",
83+
g.QueryGroupID, g.QueryGroupName, q.QueryID, q.QueryCode, q.QueryName, q.QueryType, formatQueryForm(q),
84+
)
85+
}
86+
}
87+
return nil
88+
})
89+
}
90+
91+
// formatQueryForm renders the form column for `query list`: single form shows
92+
// "<name> (fid)"; multi-form shows form count and a "+" suffix.
93+
func formatQueryForm(q xpoint.Query) string {
94+
if q.FormCount > 1 {
95+
return fmt.Sprintf("%d forms", q.FormCount)
96+
}
97+
if q.FormName == "" {
98+
return "-"
99+
}
100+
return fmt.Sprintf("%s (%d)", q.FormName, q.FID)
101+
}
102+
103+
func runQueryExec(cmd *cobra.Command, args []string) error {
104+
queryCode := strings.TrimSpace(args[0])
105+
if queryCode == "" {
106+
return fmt.Errorf("query_code is required")
107+
}
108+
client, err := newClientFromFlags(cmd.Context())
109+
if err != nil {
110+
return err
111+
}
112+
p := xpoint.GetQueryParams{ExecFlag: !queryExecNoRun}
113+
if cmd.Flags().Changed("rows") {
114+
v := queryExecRows
115+
p.Rows = &v
116+
}
117+
if cmd.Flags().Changed("offset") {
118+
v := queryExecOffset
119+
p.Offset = &v
120+
}
121+
122+
raw, err := client.GetQuery(cmd.Context(), queryCode, p)
123+
if err != nil {
124+
return err
125+
}
126+
if queryExecJQ != "" {
127+
return runJQ(raw, queryExecJQ)
128+
}
129+
return writeJSON(os.Stdout, raw)
130+
}

cmd/schema.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ Supported aliases map to the CLI's commands:
2626
document.delete DELETE /api/v1/documents/{docid}
2727
document.download GET /api/v1/documents/{docid}/pdf
2828
document.status GET /api/v1/documents/{docid}/status
29+
query.list GET /api/v1/query/
30+
query.exec GET /api/v1/query/{query_code}
2931
3032
Run without arguments to list supported aliases.`,
3133
Args: cobra.MaximumNArgs(1),

internal/schema/query.exec.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"method": "GET",
3+
"path": "/api/v1/query/{query_code}",
4+
"summary": "クエリ取得/実行",
5+
"description": "認証ユーザーが閲覧できるクエリをひとつ選択し、その設定情報を取得する。exec_flg=true の場合は実行結果も返す。rows / offset で一覧クエリのページングが可能。抽出条件設定にしたがって絞り込まれた状態で返る。\n",
6+
"parameters": [
7+
{
8+
"name": "query_code",
9+
"in": "path",
10+
"type": "string",
11+
"required": true,
12+
"description": "クエリコード"
13+
},
14+
{
15+
"name": "exec_flg",
16+
"in": "query",
17+
"type": "boolean",
18+
"required": false,
19+
"description": "true: クエリ実行 (定義と実行結果) / false: 定義のみ。既定は false。"
20+
},
21+
{
22+
"name": "rows",
23+
"in": "query",
24+
"type": "integer",
25+
"required": false,
26+
"description": "出力行数。既定は 500。1〜10000 で指定可能。"
27+
},
28+
{
29+
"name": "offset",
30+
"in": "query",
31+
"type": "integer",
32+
"required": false,
33+
"description": "オフセット値。一覧クエリのみ有効。既定は 0。"
34+
}
35+
],
36+
"response": {
37+
"type": "object",
38+
"description": "クエリ情報および実行結果。レスポンス形状は query_type によって異なる (list / summary / cross)。",
39+
"properties": {
40+
"query": {
41+
"type": "object",
42+
"description": "クエリ定義 (query_id, query_code, query_name, query_type, form_count, fid/forms, csv_filename, graph など)"
43+
},
44+
"exec_result": {
45+
"type": "object",
46+
"description": "実行結果。exec_flg=true の場合に返る。list の場合は data: [{docid, record: {...}}] の形。summary/cross の場合はヘッダ・行情報が含まれる。"
47+
}
48+
}
49+
}
50+
}

internal/schema/query.list.json

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
{
2+
"method": "GET",
3+
"path": "/api/v1/query/",
4+
"summary": "利用可能クエリ一覧取得",
5+
"description": "認証ユーザーが利用できるクエリ情報の一覧を取得する。\n",
6+
"response": {
7+
"type": "object",
8+
"properties": {
9+
"query_groups": {
10+
"type": "array",
11+
"description": "クエリグループ情報。取得結果が 1 件も無い場合は空配列。",
12+
"items": {
13+
"type": "object",
14+
"properties": {
15+
"query_group_id": {
16+
"type": "integer",
17+
"description": "クエリグループID。マイクエリの場合は 0 固定"
18+
},
19+
"query_group_name": {
20+
"type": "string",
21+
"description": "クエリグループ名称"
22+
},
23+
"queries": {
24+
"type": "array",
25+
"description": "クエリグループに属するクエリの一覧",
26+
"items": {
27+
"type": "object",
28+
"properties": {
29+
"query_id": {
30+
"type": "integer",
31+
"description": "クエリID"
32+
},
33+
"query_code": {
34+
"type": "string",
35+
"description": "クエリコード"
36+
},
37+
"query_name": {
38+
"type": "string",
39+
"description": "クエリ名"
40+
},
41+
"query_type": {
42+
"type": "string",
43+
"description": "クエリ種別 (list: 一覧 / summary: サマリ / cross: クロス集計)"
44+
},
45+
"query_type_name": {
46+
"type": "string",
47+
"description": "クエリ種別名称"
48+
},
49+
"remarks": {
50+
"type": "string",
51+
"description": "備考"
52+
},
53+
"form_count": {
54+
"type": "integer",
55+
"description": "参照するフォームの件数"
56+
},
57+
"fid": {
58+
"type": "integer",
59+
"description": "フォームID (form_count が 1 の場合)"
60+
},
61+
"form_name": {
62+
"type": "string",
63+
"description": "フォーム名 (form_count が 1 の場合)"
64+
},
65+
"forms": {
66+
"type": "array",
67+
"description": "フォーム情報 (form_count が 2 以上の場合)",
68+
"items": {
69+
"type": "object",
70+
"properties": {
71+
"fid": {
72+
"type": "integer"
73+
},
74+
"form_name": {
75+
"type": "string"
76+
}
77+
}
78+
}
79+
}
80+
}
81+
}
82+
}
83+
}
84+
}
85+
}
86+
}
87+
}
88+
}

internal/schema/schema_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ func TestAliases_Sorted(t *testing.T) {
1818
"document.update",
1919
"form.list",
2020
"form.show",
21+
"query.exec",
22+
"query.list",
2123
}
2224
if len(got) != len(want) {
2325
t.Fatalf("aliases = %v", got)
@@ -141,6 +143,40 @@ func TestLookup_DocumentDownload(t *testing.T) {
141143
}
142144
}
143145

146+
func TestLookup_QueryList(t *testing.T) {
147+
op, err := Lookup("query.list")
148+
if err != nil {
149+
t.Fatalf("Lookup: %v", err)
150+
}
151+
if op["method"] != "GET" {
152+
t.Errorf("method = %v", op["method"])
153+
}
154+
if op["path"] != "/api/v1/query/" {
155+
t.Errorf("path = %v", op["path"])
156+
}
157+
}
158+
159+
func TestLookup_QueryExec(t *testing.T) {
160+
op, err := Lookup("query.exec")
161+
if err != nil {
162+
t.Fatalf("Lookup: %v", err)
163+
}
164+
if op["method"] != "GET" {
165+
t.Errorf("method = %v", op["method"])
166+
}
167+
if op["path"] != "/api/v1/query/{query_code}" {
168+
t.Errorf("path = %v", op["path"])
169+
}
170+
params, _ := op["parameters"].([]any)
171+
if len(params) < 1 {
172+
t.Fatalf("parameters = %v", params)
173+
}
174+
first, _ := params[0].(map[string]any)
175+
if first["name"] != "query_code" || first["required"] != true {
176+
t.Errorf("first param = %v", first)
177+
}
178+
}
179+
144180
func TestLookup_DocumentStatus(t *testing.T) {
145181
op, err := Lookup("document.status")
146182
if err != nil {

0 commit comments

Comments
 (0)