snan-cli 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,6 +67,12 @@ astro metadata tableDetail --fqn=tenant-doris_prod.default.prod_dwi.dwi_order
67
67
  ### 数据资产
68
68
 
69
69
  ```bash
70
+ # 查询 L3 业务模型;结果包含所属业务域、业务阶段及关联资产概要
71
+ astro asset businessModels --keyword="销售|订单"
72
+
73
+ # name 必须来自 businessModels 返回结果
74
+ astro asset businessModelDetail --name="销售订单处理"
75
+
70
76
  astro asset dataModels --keyword="销售|订单"
71
77
  astro asset dataModelDetail --nameEn=SalesOrder
72
78
  astro asset indicators --keyword="GMV|成交额"
@@ -91,6 +97,19 @@ astro data queryOrigin --body="{\"dsCode\":\"erp_prod\",\"sql\":\"SELECT order_n
91
97
 
92
98
  `dsCode` 必须来自 `metadata sources`,并且对应 `sourceType=DATASOURCE` 的来源。
93
99
 
100
+ ### 组织查询
101
+
102
+ ```bash
103
+ # 获取部门清单
104
+ astro org departments --raw
105
+
106
+ # 按关键字、部门筛选人员
107
+ astro org users --keyword '张三' --dept-code D001 --page 1 --page-size 100 --raw
108
+
109
+ # 按角色名称筛选角色
110
+ astro org roles --keyword '销售' --page 1 --page-size 1000 --raw
111
+ ```
112
+
94
113
  ### 数据标准
95
114
 
96
115
  ```bash
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "snan-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "CLI tool to expose backend HTTP interfaces as command-line commands",
5
5
  "type": "commonjs",
6
6
  "bin": {
7
- "astro": "./bin/astro"
7
+ "astro": "bin/astro"
8
8
  },
9
9
  "main": "src/index.js",
10
10
  "files": [
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "start": "node bin/astro",
18
- "test": "node test/cli.test.js"
18
+ "test": "node --test"
19
19
  },
20
20
  "keywords": [
21
21
  "cli",
package/src/index.js CHANGED
@@ -34,6 +34,15 @@ function setupCompletion(modules) {
34
34
  }
35
35
  }
36
36
 
37
+ function parseOptionValue(param, value) {
38
+ if (param.type !== 'integer') return value;
39
+ const parsed = Number.parseInt(value, 10);
40
+ if (!Number.isInteger(parsed) || parsed <= 0) {
41
+ throw new Error(`--${param.flag || param.name} must be a positive integer`);
42
+ }
43
+ return parsed;
44
+ }
45
+
37
46
  async function main() {
38
47
  const modules = loadModules();
39
48
 
@@ -86,13 +95,15 @@ async function main() {
86
95
 
87
96
  const method = (actionDef.method || 'POST').toUpperCase();
88
97
  const paramDefs = actionDef.params || [];
98
+ const bodyParam = method === 'GET' ? null : paramDefs.find(p => p.name === 'body');
99
+ const optionParamDefs = bodyParam
100
+ ? paramDefs.filter(p => p.name !== 'body')
101
+ : paramDefs;
89
102
 
90
- if (method === 'GET') {
91
- paramDefs.forEach(p => {
92
- cmd.option(`--${p.name} <value>`, p.description);
93
- });
94
- } else {
95
- const bodyParam = paramDefs.find(p => p.name === 'body');
103
+ optionParamDefs.forEach(p => {
104
+ cmd.option(`--${p.flag || p.name} <value>`, p.description);
105
+ });
106
+ if (method !== 'GET') {
96
107
  const bodyDescription = bodyParam?.description || 'JSON body for the request';
97
108
  if (bodyParam?.required) {
98
109
  cmd.requiredOption('--body <json>', bodyDescription);
@@ -107,35 +118,38 @@ async function main() {
107
118
  try {
108
119
  // action.path 优先(支持子路径),否则自动拼接 /actionName
109
120
  const url = actionDef.path ? `${urlPrefix}${actionDef.path}` : `${urlPrefix}/${actionName}`;
110
- let requestParams = {};
111
-
112
- if (method === 'GET') {
113
- paramDefs.forEach(p => {
114
- if (options[p.name] !== undefined) {
115
- requestParams[p.name] = options[p.name];
116
- } else if (p.required) {
117
- console.error(`Error: --${p.name} is required`);
118
- process.exit(1);
119
- }
120
- });
121
- } else {
122
- if (options.body) {
123
- try {
124
- requestParams = JSON.parse(options.body);
125
- } catch (err) {
126
- console.error(`Error: Invalid JSON in --body: ${err.message}`);
127
- process.exit(1);
128
- }
121
+ let requestParams = { ...(actionDef.requestDefaults || {}) };
122
+
123
+ if (method !== 'GET' && options.body) {
124
+ try {
125
+ requestParams = { ...requestParams, ...JSON.parse(options.body) };
126
+ } catch (err) {
127
+ console.error(`Error: Invalid JSON in --body: ${err.message}`);
128
+ process.exit(1);
129
129
  }
130
+ } else if (bodyParam && bodyParam.required) {
131
+ throw new Error('--body is required');
130
132
  }
131
133
 
132
- const response = await httpClient.request2(method, url, requestParams);
134
+ optionParamDefs.forEach(p => {
135
+ if (options[p.name] !== undefined) {
136
+ requestParams[p.requestName || p.name] = parseOptionValue(p, options[p.name]);
137
+ } else if (p.required) {
138
+ throw new Error(`--${p.flag || p.name} is required`);
139
+ }
140
+ });
141
+
142
+ let response = await httpClient.request2(method, url, requestParams);
133
143
 
134
144
  if (response && response.isHttpError) {
135
145
  printError(response);
136
146
  process.exit(1);
137
147
  }
138
148
 
149
+ if (actionDef.transformResponse) {
150
+ response = actionDef.transformResponse(response);
151
+ }
152
+
139
153
  print(response, { raw: options.raw });
140
154
  } catch (err) {
141
155
  printError(err);
@@ -152,3 +166,5 @@ main().catch(err => {
152
166
  console.error('Fatal error:', err.message);
153
167
  process.exit(1);
154
168
  });
169
+
170
+ module.exports = { parseOptionValue };
@@ -1,8 +1,22 @@
1
1
  module.exports = {
2
2
  name: 'asset',
3
- description: '数据资产:数据模型和指标查询',
3
+ description: '数据资产:业务模型、数据模型和指标查询',
4
4
  urlPrefix: '/astro-server/openapi/v1/asset',
5
5
  actions: {
6
+ businessModels: {
7
+ description: '获取 L3 业务模型列表(含所属业务域、业务阶段及关联资产概要)',
8
+ method: 'GET',
9
+ params: [
10
+ { name: 'keyword', description: '查询关键字,多个关键字用空格或|分隔,为空查询全部', required: false },
11
+ ],
12
+ },
13
+ businessModelDetail: {
14
+ description: '根据业务模型名称获取详情(含业务定义、层级及关联资产)',
15
+ method: 'GET',
16
+ params: [
17
+ { name: 'name', description: '业务模型名称(唯一标识),必须来自 businessModels 返回结果', required: true },
18
+ ],
19
+ },
6
20
  dataModels: {
7
21
  description: '获取数据资产模型列表(含可用表信息)',
8
22
  method: 'GET',
@@ -0,0 +1,65 @@
1
+ function mapData(response, mapper) {
2
+ if (!response || response.data === undefined) return response;
3
+ return { ...response, data: mapper(response.data) };
4
+ }
5
+
6
+ function roleSummary(role) {
7
+ return {
8
+ code: role && role.code,
9
+ roleName: role && role.roleName,
10
+ };
11
+ }
12
+
13
+ module.exports = {
14
+ name: 'org',
15
+ description: '组织查询:部门、人员和角色只读清单',
16
+ urlPrefix: '/astro-server/openapi/v1/org',
17
+ actions: {
18
+ departments: {
19
+ description: '获取当前租户的部门清单',
20
+ method: 'GET',
21
+ path: '/departments',
22
+ transformResponse: response => mapData(response, data => (data || []).map(item => ({
23
+ code: item.code,
24
+ name: item.name,
25
+ }))),
26
+ },
27
+ users: {
28
+ description: '获取当前租户的人员清单',
29
+ method: 'GET',
30
+ path: '/users',
31
+ requestDefaults: { pageNo: 1, pageSize: 100 },
32
+ params: [
33
+ { name: 'keyword', description: '姓名或账号关键字' },
34
+ { name: 'deptCode', flag: 'dept-code', description: '部门编码' },
35
+ { name: 'page', requestName: 'pageNo', flag: 'page', description: '页码,默认 1', type: 'integer' },
36
+ { name: 'pageSize', flag: 'page-size', description: '每页数量,默认 100', type: 'integer' },
37
+ ],
38
+ transformResponse: response => mapData(response, data => ({
39
+ ...data,
40
+ result: (data && data.result ? data.result : []).map(user => ({
41
+ account: user.account,
42
+ name: user.name,
43
+ deptCode: user.deptCode,
44
+ deptName: user.deptName,
45
+ roles: (user.roles || []).map(roleSummary),
46
+ })),
47
+ })),
48
+ },
49
+ roles: {
50
+ description: '获取当前租户的角色清单',
51
+ method: 'GET',
52
+ path: '/roles',
53
+ requestDefaults: { pageNo: 1, pageSize: 1000 },
54
+ params: [
55
+ { name: 'keyword', description: '角色名称关键字' },
56
+ { name: 'page', requestName: 'pageNo', flag: 'page', description: '页码,默认 1', type: 'integer' },
57
+ { name: 'pageSize', flag: 'page-size', description: '每页数量,默认 1000', type: 'integer' },
58
+ ],
59
+ transformResponse: response => mapData(response, data => ({
60
+ ...data,
61
+ result: (data && data.result ? data.result : []).map(roleSummary),
62
+ })),
63
+ },
64
+ },
65
+ };