@kweaver-ai/kweaver-sdk 0.4.10 → 0.4.12

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
@@ -90,6 +90,29 @@ const graph = await client.bkn.querySubgraph("bkn-id", { /* path spec */ });
90
90
  await client.bkn.executeAction("bkn-id", "at-id", { /* params */ });
91
91
  const logs = await client.bkn.listActionLogs("bkn-id");
92
92
 
93
+ // Data sources & data views
94
+ const dsList = await client.datasources.list();
95
+ const tables = await client.datasources.listTables("ds-id");
96
+ const viewId = await client.dataviews.create({ name: "v", datasourceId: "ds-id", table: "orders" });
97
+ const views = await client.dataviews.list({ datasourceId: "ds-id" });
98
+ const fuzzy = await client.dataviews.find("BOM", { wait: false });
99
+ const exact = await client.dataviews.find("orders", {
100
+ datasourceId: "ds-id",
101
+ exact: true,
102
+ wait: true,
103
+ });
104
+ const dv = await client.dataviews.get(viewId);
105
+
106
+ // Dataflow automation (CSV import pipeline, etc.)
107
+ const result = await client.dataflows.execute({
108
+ title: "import", trigger_config: { operator: "manual" },
109
+ steps: [{ id: "s1", title: "load", operator: "csv_import", parameters: {} }],
110
+ });
111
+
112
+ // Vega observability
113
+ const catalogs = await client.vega.listCatalogs();
114
+ const health = await client.vega.health();
115
+
93
116
  // Context Loader (semantic search over a BKN via MCP)
94
117
  const cl = client.contextLoader(mcpUrl, "bkn-id");
95
118
  const results = await cl.search({ query: "hypertension treatment" });
@@ -98,16 +121,24 @@ const results = await cl.search({ query: "hypertension treatment" });
98
121
  ## CLI Reference
99
122
 
100
123
  ```
101
- kweaver auth login <url> [--alias name] [-u user] [-p pass] [--playwright] — also: status, list, use, delete, logout
124
+ kweaver auth login <url> [--alias name] [-u user] [-p pass] [--playwright] [--insecure|-k] — also: status, list, use, delete, logout
102
125
  kweaver token
126
+ kweaver config show / set-bd <value>
127
+ kweaver ds list/get/delete/tables/connect
128
+ kweaver ds import-csv <ds_id> --files <glob> [--table-prefix <p>] [--batch-size 500]
129
+ kweaver dataview list/find/get/delete
103
130
  kweaver bkn list/get/stats/export/create/update/delete
131
+ kweaver bkn create-from-ds <ds_id> --name <name> [--tables t1,t2] [--build]
132
+ kweaver bkn create-from-csv <ds_id> --files <glob> --name <name> [--build]
133
+ kweaver bkn validate/push/pull
104
134
  kweaver bkn object-type list/get/create/update/delete/query/properties
105
135
  kweaver bkn relation-type list/get/create/update/delete
106
136
  kweaver bkn action-type list/query/execute
107
- kweaver bkn subgraph
137
+ kweaver bkn subgraph / search
108
138
  kweaver bkn action-execution get
109
139
  kweaver bkn action-log list/get/cancel
110
- kweaver agent list/get/chat/sessions/history
140
+ kweaver agent list/get/create/update/delete/chat/sessions/history/publish/unpublish
141
+ kweaver vega health/stats/inspect/catalog/resource/connector-type
111
142
  kweaver context-loader config set/use/list/show
112
143
  kweaver context-loader kn-search/query-object-instance/...
113
144
  kweaver call <path> [-X METHOD] [-d BODY] [-H header]
@@ -120,6 +151,33 @@ kweaver call <path> [-X METHOD] [-d BODY] [-H header]
120
151
  | `KWEAVER_BASE_URL` | KWeaver instance URL |
121
152
  | `KWEAVER_BUSINESS_DOMAIN` | Business domain identifier |
122
153
  | `KWEAVER_TOKEN` | Access token |
154
+ | `KWEAVER_TLS_INSECURE` | Set to `1` or `true` to skip TLS certificate verification for all HTTPS in the process (dev only; prefer `kweaver auth … --insecure` which saves per platform) |
155
+ | `NODE_TLS_REJECT_UNAUTHORIZED` | Node.js built-in TLS switch: set to `0` to skip certificate verification for HTTPS in this process. The `kweaver` CLI sets this when `KWEAVER_TLS_INSECURE` is set or the saved token has insecure TLS (same scope as above; dev only). |
156
+
157
+ ### TLS Certificate Troubleshooting
158
+
159
+ If you encounter errors like `fetch failed`, `self-signed certificate`, or `UNABLE_TO_GET_ISSUER_CERT`, the target server likely uses a self-signed certificate or Kubernetes Ingress default fake certificate. Try the following in order of preference:
160
+
161
+ 1. **Recommended (persists per platform)** — add `--insecure` during login:
162
+ ```bash
163
+ kweaver auth login https://your-host --insecure
164
+ # or shorthand
165
+ kweaver auth login https://your-host -k
166
+ ```
167
+ The flag is saved to `token.json` in `~/.kweaver/`, so all subsequent CLI commands for that platform skip TLS verification automatically.
168
+
169
+ 2. **Temporary (current shell)** — set an environment variable:
170
+ ```bash
171
+ export KWEAVER_TLS_INSECURE=1
172
+ kweaver bkn list
173
+ ```
174
+
175
+ 3. **Node.js native** — set `NODE_TLS_REJECT_UNAUTHORIZED` directly:
176
+ ```bash
177
+ NODE_TLS_REJECT_UNAUTHORIZED=0 kweaver bkn list
178
+ ```
179
+
180
+ > **Security note:** All of the above disable HTTPS certificate verification and should only be used in development or internal network environments. Use trusted CA-signed certificates in production.
123
181
 
124
182
  ## Using with AI Agents
125
183
 
package/README.zh.md CHANGED
@@ -90,6 +90,18 @@ const graph = await client.bkn.querySubgraph("bkn-id", { /* 路径规格 */
90
90
  await client.bkn.executeAction("bkn-id", "at-id", { /* 参数 */ });
91
91
  const logs = await client.bkn.listActionLogs("bkn-id");
92
92
 
93
+ // 数据源与数据视图
94
+ const dsList = await client.datasources.list();
95
+ const viewId = await client.dataviews.create({ name: "v", datasourceId: "ds-id", table: "orders" });
96
+ const views = await client.dataviews.list({ datasourceId: "ds-id" });
97
+ const fuzzy = await client.dataviews.find("BOM", { wait: false });
98
+ const exact = await client.dataviews.find("orders", {
99
+ datasourceId: "ds-id",
100
+ exact: true,
101
+ wait: true,
102
+ });
103
+ const dv = await client.dataviews.get(viewId);
104
+
93
105
  // Context Loader(通过 MCP 对 BKN 做语义搜索)
94
106
  const cl = client.contextLoader(mcpUrl, "bkn-id");
95
107
  const results = await cl.search({ query: "高血压 治疗" });
@@ -98,8 +110,10 @@ const results = await cl.search({ query: "高血压 治疗" });
98
110
  ## 命令速查
99
111
 
100
112
  ```
101
- kweaver auth login <url> [--alias name] [-u user] [-p pass] [--playwright] — 另有 status、list、use、delete、logout
113
+ kweaver auth login <url> [--alias name] [-u user] [-p pass] [--playwright] [--insecure|-k] — 另有 status、list、use、delete、logout
102
114
  kweaver token
115
+ kweaver ds list/get/delete/tables/connect
116
+ kweaver dataview list/find/get/delete
103
117
  kweaver bkn list/get/stats/export/create/update/delete
104
118
  kweaver bkn object-type list/get/create/update/delete/query/properties
105
119
  kweaver bkn relation-type list/get/create/update/delete
@@ -120,6 +134,33 @@ kweaver call <path> [-X METHOD] [-d BODY] [-H header]
120
134
  | `KWEAVER_BASE_URL` | KWeaver 实例地址 |
121
135
  | `KWEAVER_BUSINESS_DOMAIN` | 业务域标识 |
122
136
  | `KWEAVER_TOKEN` | 访问令牌 |
137
+ | `KWEAVER_TLS_INSECURE` | 设为 `1` 或 `true` 时跳过 TLS 证书校验(仅开发;更推荐 `kweaver auth … --insecure` 以按平台持久化) |
138
+ | `NODE_TLS_REJECT_UNAUTHORIZED` | Node.js 内置 TLS 开关:设为 `0` 时在本进程内跳过 HTTPS 证书校验。`kweaver` 在 `KWEAVER_TLS_INSECURE` 生效或已保存 token 为不安全 TLS 时会设置此项(范围同上;仅开发)。 |
139
+
140
+ ### TLS 证书问题排查
141
+
142
+ 如果遇到 `fetch failed`、`self-signed certificate`、`UNABLE_TO_GET_ISSUER_CERT` 等 TLS 相关错误,通常是目标服务器使用了自签名证书或 Kubernetes Ingress 默认假证书。可按优先级尝试以下方案:
143
+
144
+ 1. **推荐(按平台持久化)** — 登录时加 `--insecure`:
145
+ ```bash
146
+ kweaver auth login https://your-host --insecure
147
+ # 或简写
148
+ kweaver auth login https://your-host -k
149
+ ```
150
+ 该标记会写入 `~/.kweaver/` 的 `token.json`,后续所有 CLI 命令对该平台自动生效,无需额外操作。
151
+
152
+ 2. **临时生效(当前 shell)** — 设置环境变量:
153
+ ```bash
154
+ export KWEAVER_TLS_INSECURE=1
155
+ kweaver bkn list
156
+ ```
157
+
158
+ 3. **Node.js 原生方式** — 直接设置 `NODE_TLS_REJECT_UNAUTHORIZED`:
159
+ ```bash
160
+ NODE_TLS_REJECT_UNAUTHORIZED=0 kweaver bkn list
161
+ ```
162
+
163
+ > **安全提示:** 以上方式均会跳过 HTTPS 证书校验,仅适用于开发/内网环境。生产环境请使用受信任的 CA 签发证书。
123
164
 
124
165
  ## 在 AI 智能体中使用
125
166
 
@@ -0,0 +1,78 @@
1
+ export interface DataflowStep {
2
+ id: string;
3
+ title: string;
4
+ operator: string;
5
+ parameters: Record<string, unknown>;
6
+ }
7
+ export interface DataflowCreateBody {
8
+ title: string;
9
+ description?: string;
10
+ trigger_config: {
11
+ operator: string;
12
+ };
13
+ steps: DataflowStep[];
14
+ }
15
+ export interface DataflowResult {
16
+ status: "success" | "completed" | "failed" | "error";
17
+ reason?: string;
18
+ }
19
+ export interface CreateDataflowOptions {
20
+ baseUrl: string;
21
+ accessToken: string;
22
+ businessDomain?: string;
23
+ body: DataflowCreateBody;
24
+ }
25
+ /**
26
+ * Create a new dataflow (DAG). Returns the new DAG id.
27
+ */
28
+ export declare function createDataflow(options: CreateDataflowOptions): Promise<string>;
29
+ export interface RunDataflowOptions {
30
+ baseUrl: string;
31
+ accessToken: string;
32
+ businessDomain?: string;
33
+ dagId: string;
34
+ }
35
+ /**
36
+ * Trigger a run for an existing dataflow DAG.
37
+ */
38
+ export declare function runDataflow(options: RunDataflowOptions): Promise<void>;
39
+ export interface PollDataflowOptions {
40
+ baseUrl: string;
41
+ accessToken: string;
42
+ businessDomain?: string;
43
+ dagId: string;
44
+ /** Poll interval in seconds. Default: 3 */
45
+ interval?: number;
46
+ /** Maximum time to wait in seconds. Default: 900 */
47
+ timeout?: number;
48
+ }
49
+ /**
50
+ * Poll GET /api/automation/v1/dag/{dagId}/results until the run is done.
51
+ * Throws on "failed"/"error" status or timeout.
52
+ */
53
+ export declare function pollDataflowResults(options: PollDataflowOptions): Promise<DataflowResult>;
54
+ export interface DeleteDataflowOptions {
55
+ baseUrl: string;
56
+ accessToken: string;
57
+ businessDomain?: string;
58
+ dagId: string;
59
+ }
60
+ /**
61
+ * Delete a dataflow DAG. Best-effort — does not throw on errors.
62
+ */
63
+ export declare function deleteDataflow(options: DeleteDataflowOptions): Promise<void>;
64
+ export interface ExecuteDataflowOptions {
65
+ baseUrl: string;
66
+ accessToken: string;
67
+ businessDomain?: string;
68
+ body: DataflowCreateBody;
69
+ /** Poll interval in seconds. Default: 3 */
70
+ interval?: number;
71
+ /** Maximum polling time in seconds. Default: 900 */
72
+ timeout?: number;
73
+ }
74
+ /**
75
+ * Full dataflow lifecycle: create → run → poll → delete (always, even on error).
76
+ * Returns the final DataflowResult.
77
+ */
78
+ export declare function executeDataflow(options: ExecuteDataflowOptions): Promise<DataflowResult>;
@@ -0,0 +1,135 @@
1
+ import { HttpError } from "../utils/http.js";
2
+ function buildHeaders(accessToken, businessDomain) {
3
+ return {
4
+ accept: "application/json, text/plain, */*",
5
+ "accept-language": "zh-cn",
6
+ authorization: `Bearer ${accessToken}`,
7
+ token: accessToken,
8
+ "x-business-domain": businessDomain,
9
+ "x-language": "zh-cn",
10
+ };
11
+ }
12
+ function debugLog(method, url, headers, body) {
13
+ if (!process.env["KWEAVER_DEBUG_HTTP"])
14
+ return;
15
+ const masked = { ...headers };
16
+ if (masked.authorization)
17
+ masked.authorization = masked.authorization.slice(0, 20) + "…";
18
+ if (masked.token)
19
+ masked.token = masked.token.slice(0, 20) + "…";
20
+ process.stderr.write(`[debug] ${method} ${url}\n`);
21
+ process.stderr.write(`[debug] headers: ${JSON.stringify(masked)}\n`);
22
+ if (body)
23
+ process.stderr.write(`[debug] body (first 300): ${body.slice(0, 300)}\n`);
24
+ }
25
+ /**
26
+ * Create a new dataflow (DAG). Returns the new DAG id.
27
+ */
28
+ export async function createDataflow(options) {
29
+ const { baseUrl, accessToken, businessDomain = "bd_public", body } = options;
30
+ const base = baseUrl.replace(/\/+$/, "");
31
+ const url = `${base}/api/automation/v1/data-flow/flow`;
32
+ const reqHeaders = { ...buildHeaders(accessToken, businessDomain), "content-type": "application/json" };
33
+ const reqBody = JSON.stringify(body);
34
+ debugLog("POST", url, reqHeaders, reqBody);
35
+ const response = await fetch(url, {
36
+ method: "POST",
37
+ headers: reqHeaders,
38
+ body: reqBody,
39
+ });
40
+ const responseBody = await response.text();
41
+ if (!response.ok) {
42
+ throw new HttpError(response.status, response.statusText, responseBody);
43
+ }
44
+ const parsed = JSON.parse(responseBody);
45
+ return parsed.id;
46
+ }
47
+ /**
48
+ * Trigger a run for an existing dataflow DAG.
49
+ */
50
+ export async function runDataflow(options) {
51
+ const { baseUrl, accessToken, businessDomain = "bd_public", dagId } = options;
52
+ const base = baseUrl.replace(/\/+$/, "");
53
+ const url = `${base}/api/automation/v1/run-instance/${encodeURIComponent(dagId)}`;
54
+ const response = await fetch(url, {
55
+ method: "POST",
56
+ headers: {
57
+ ...buildHeaders(accessToken, businessDomain),
58
+ "content-type": "application/json",
59
+ },
60
+ body: "{}",
61
+ });
62
+ if (!response.ok) {
63
+ const responseBody = await response.text();
64
+ throw new HttpError(response.status, response.statusText, responseBody);
65
+ }
66
+ }
67
+ /**
68
+ * Poll GET /api/automation/v1/dag/{dagId}/results until the run is done.
69
+ * Throws on "failed"/"error" status or timeout.
70
+ */
71
+ export async function pollDataflowResults(options) {
72
+ const { baseUrl, accessToken, businessDomain = "bd_public", dagId, interval = 3, timeout = 900, } = options;
73
+ const base = baseUrl.replace(/\/+$/, "");
74
+ const url = `${base}/api/automation/v1/dag/${encodeURIComponent(dagId)}/results`;
75
+ const deadlineMs = Date.now() + timeout * 1000;
76
+ while (Date.now() < deadlineMs) {
77
+ const response = await fetch(url, {
78
+ method: "GET",
79
+ headers: buildHeaders(accessToken, businessDomain),
80
+ });
81
+ const responseBody = await response.text();
82
+ if (!response.ok) {
83
+ throw new HttpError(response.status, response.statusText, responseBody);
84
+ }
85
+ const parsed = JSON.parse(responseBody);
86
+ const results = parsed.results ?? [];
87
+ const latest = results[0];
88
+ if (latest) {
89
+ if (latest.status === "success" || latest.status === "completed") {
90
+ return latest;
91
+ }
92
+ if (latest.status === "failed" || latest.status === "error") {
93
+ const reason = latest.reason ? `: ${latest.reason}` : "";
94
+ throw new Error(`Dataflow run ${latest.status}${reason}`);
95
+ }
96
+ }
97
+ // Still running — wait before next poll
98
+ if (interval > 0) {
99
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
100
+ }
101
+ }
102
+ throw new Error(`Dataflow polling timed out after ${timeout}s for DAG ${dagId}`);
103
+ }
104
+ /**
105
+ * Delete a dataflow DAG. Best-effort — does not throw on errors.
106
+ */
107
+ export async function deleteDataflow(options) {
108
+ const { baseUrl, accessToken, businessDomain = "bd_public", dagId } = options;
109
+ const base = baseUrl.replace(/\/+$/, "");
110
+ const url = `${base}/api/automation/v1/data-flow/flow/${encodeURIComponent(dagId)}`;
111
+ try {
112
+ await fetch(url, {
113
+ method: "DELETE",
114
+ headers: buildHeaders(accessToken, businessDomain),
115
+ });
116
+ }
117
+ catch {
118
+ // Best-effort: swallow all errors
119
+ }
120
+ }
121
+ /**
122
+ * Full dataflow lifecycle: create → run → poll → delete (always, even on error).
123
+ * Returns the final DataflowResult.
124
+ */
125
+ export async function executeDataflow(options) {
126
+ const { baseUrl, accessToken, businessDomain = "bd_public", body, interval, timeout } = options;
127
+ const dagId = await createDataflow({ baseUrl, accessToken, businessDomain, body });
128
+ try {
129
+ await runDataflow({ baseUrl, accessToken, businessDomain, dagId });
130
+ return await pollDataflowResults({ baseUrl, accessToken, businessDomain, dagId, interval, timeout });
131
+ }
132
+ finally {
133
+ await deleteDataflow({ baseUrl, accessToken, businessDomain, dagId }).catch(() => { });
134
+ }
135
+ }
@@ -1,3 +1,19 @@
1
+ /** Field metadata returned by the data-views API. */
2
+ export interface ViewField {
3
+ name: string;
4
+ type: string;
5
+ display_name?: string;
6
+ comment?: string;
7
+ }
8
+ /** Normalized data view model (mdl-data-model). */
9
+ export interface DataView {
10
+ id: string;
11
+ name: string;
12
+ query_type: string;
13
+ datasource_id: string;
14
+ fields: ViewField[];
15
+ }
16
+ export declare function parseDataView(raw: Record<string, unknown>): DataView;
1
17
  export interface CreateDataViewOptions {
2
18
  baseUrl: string;
3
19
  accessToken: string;
@@ -11,10 +27,51 @@ export interface CreateDataViewOptions {
11
27
  businessDomain?: string;
12
28
  }
13
29
  export declare function createDataView(options: CreateDataViewOptions): Promise<string>;
30
+ export interface ListDataViewsOptions {
31
+ baseUrl: string;
32
+ accessToken: string;
33
+ businessDomain?: string;
34
+ /** Filter by data source id. */
35
+ datasourceId?: string;
36
+ /** Server-side keyword filter (fuzzy). */
37
+ name?: string;
38
+ /** View type filter (e.g. atomic, custom). */
39
+ type?: string;
40
+ /** Max items; default -1 (all). */
41
+ limit?: number;
42
+ }
43
+ export declare function listDataViews(options: ListDataViewsOptions): Promise<DataView[]>;
44
+ export interface DeleteDataViewOptions {
45
+ baseUrl: string;
46
+ accessToken: string;
47
+ id: string;
48
+ businessDomain?: string;
49
+ }
50
+ export declare function deleteDataView(options: DeleteDataViewOptions): Promise<void>;
14
51
  export interface GetDataViewOptions {
15
52
  baseUrl: string;
16
53
  accessToken: string;
17
54
  id: string;
18
55
  businessDomain?: string;
19
56
  }
20
- export declare function getDataView(options: GetDataViewOptions): Promise<string>;
57
+ export declare function getDataView(options: GetDataViewOptions): Promise<DataView>;
58
+ export interface FindDataViewOptions {
59
+ baseUrl: string;
60
+ accessToken: string;
61
+ businessDomain?: string;
62
+ /** View name to search for (sent as keyword to server). */
63
+ name: string;
64
+ /** Filter by data source id. */
65
+ datasourceId?: string;
66
+ /** When true, apply client-side exact name match after keyword search (default false). */
67
+ exact?: boolean;
68
+ /** When true, poll until a result appears or timeout (default false). */
69
+ wait?: boolean;
70
+ /** Total wait budget in ms (default 30000). Only used when wait is true. */
71
+ timeoutMs?: number;
72
+ }
73
+ /**
74
+ * Find data views by name. Uses server-side keyword filtering; when `exact` is true,
75
+ * applies client-side `name ===` filter. Optional polling with exponential backoff.
76
+ */
77
+ export declare function findDataView(options: FindDataViewOptions): Promise<DataView[]>;
@@ -24,6 +24,41 @@ function extractViewId(data) {
24
24
  }
25
25
  return null;
26
26
  }
27
+ export function parseDataView(raw) {
28
+ const fieldsRaw = raw.fields;
29
+ const fields = [];
30
+ if (Array.isArray(fieldsRaw)) {
31
+ for (const f of fieldsRaw) {
32
+ if (f && typeof f === "object") {
33
+ const fr = f;
34
+ fields.push({
35
+ name: String(fr.name ?? ""),
36
+ type: String(fr.type ?? "varchar"),
37
+ display_name: fr.display_name != null ? String(fr.display_name) : undefined,
38
+ comment: fr.comment != null ? String(fr.comment) : undefined,
39
+ });
40
+ }
41
+ }
42
+ }
43
+ return {
44
+ id: String(raw.id ?? ""),
45
+ name: String(raw.name ?? ""),
46
+ query_type: String(raw.query_type ?? "SQL"),
47
+ datasource_id: String(raw.data_source_id ?? raw.group_id ?? ""),
48
+ fields,
49
+ };
50
+ }
51
+ function extractListPayload(data) {
52
+ if (Array.isArray(data))
53
+ return data;
54
+ if (data && typeof data === "object") {
55
+ const obj = data;
56
+ const items = obj.entries ?? obj.data;
57
+ if (Array.isArray(items))
58
+ return items;
59
+ }
60
+ return [];
61
+ }
27
62
  export async function createDataView(options) {
28
63
  const { baseUrl, accessToken, name, datasourceId, table, fields = [], businessDomain = "bd_public", } = options;
29
64
  const viewId = createHash("md5").update(`${datasourceId}:${table}`).digest("hex").slice(0, 35);
@@ -51,11 +86,90 @@ export async function createDataView(options) {
51
86
  });
52
87
  const responseBody = await response.text();
53
88
  if (!response.ok) {
89
+ // If DataView already exists (403 with "Existed" error code), delete and recreate
90
+ if (response.status === 403) {
91
+ try {
92
+ const errBody = JSON.parse(responseBody);
93
+ if (errBody.error_code?.includes("Existed")) {
94
+ const actualId = await findDataViewByName({ baseUrl, accessToken, name, groupId: datasourceId, businessDomain });
95
+ if (actualId && fields.length > 0) {
96
+ // Delete the bare DataView (created by scanMetadata) and recreate with fields
97
+ await deleteDataView({ baseUrl, accessToken, id: actualId, businessDomain });
98
+ const retryResponse = await fetch(url, {
99
+ method: "POST",
100
+ headers: { ...buildHeaders(accessToken, businessDomain), "content-type": "application/json" },
101
+ body,
102
+ });
103
+ if (retryResponse.ok) {
104
+ const retryBody = await retryResponse.text();
105
+ const retryId = extractViewId(JSON.parse(retryBody));
106
+ return retryId ?? viewId;
107
+ }
108
+ }
109
+ if (actualId)
110
+ return actualId;
111
+ return viewId;
112
+ }
113
+ }
114
+ catch { /* fall through to throw */ }
115
+ }
54
116
  throw new HttpError(response.status, response.statusText, responseBody);
55
117
  }
56
118
  const createdId = extractViewId(JSON.parse(responseBody));
57
119
  return createdId ?? viewId;
58
120
  }
121
+ async function findDataViewByName(options) {
122
+ const list = await listDataViews({
123
+ baseUrl: options.baseUrl,
124
+ accessToken: options.accessToken,
125
+ businessDomain: options.businessDomain,
126
+ name: options.name,
127
+ });
128
+ const match = list.find((e) => e.name === options.name && e.datasource_id === options.groupId);
129
+ return match?.id ?? null;
130
+ }
131
+ export async function listDataViews(options) {
132
+ const { baseUrl, accessToken, businessDomain = "bd_public", datasourceId, name, type, limit = -1, } = options;
133
+ const base = baseUrl.replace(/\/+$/, "");
134
+ const url = new URL(`${base}/api/mdl-data-model/v1/data-views`);
135
+ url.searchParams.set("limit", String(limit));
136
+ if (datasourceId)
137
+ url.searchParams.set("data_source_id", datasourceId);
138
+ if (name)
139
+ url.searchParams.set("keyword", name);
140
+ if (type)
141
+ url.searchParams.set("type", type);
142
+ const response = await fetch(url.toString(), {
143
+ method: "GET",
144
+ headers: buildHeaders(accessToken, businessDomain),
145
+ });
146
+ const bodyText = await response.text();
147
+ if (!response.ok) {
148
+ throw new HttpError(response.status, response.statusText, bodyText);
149
+ }
150
+ const parsed = JSON.parse(bodyText);
151
+ const items = extractListPayload(parsed);
152
+ const out = [];
153
+ for (const item of items) {
154
+ if (item && typeof item === "object") {
155
+ out.push(parseDataView(item));
156
+ }
157
+ }
158
+ return out;
159
+ }
160
+ export async function deleteDataView(options) {
161
+ const { baseUrl, accessToken, id, businessDomain = "bd_public" } = options;
162
+ const base = baseUrl.replace(/\/+$/, "");
163
+ const url = `${base}/api/mdl-data-model/v1/data-views/${encodeURIComponent(id)}`;
164
+ const response = await fetch(url, {
165
+ method: "DELETE",
166
+ headers: buildHeaders(accessToken, businessDomain),
167
+ });
168
+ const bodyText = await response.text();
169
+ if (!response.ok) {
170
+ throw new HttpError(response.status, response.statusText, bodyText);
171
+ }
172
+ }
59
173
  export async function getDataView(options) {
60
174
  const { baseUrl, accessToken, id, businessDomain = "bd_public", } = options;
61
175
  const base = baseUrl.replace(/\/+$/, "");
@@ -68,5 +182,40 @@ export async function getDataView(options) {
68
182
  if (!response.ok) {
69
183
  throw new HttpError(response.status, response.statusText, body);
70
184
  }
71
- return body;
185
+ let parsed = JSON.parse(body);
186
+ if (Array.isArray(parsed) && parsed.length > 0) {
187
+ parsed = parsed[0];
188
+ }
189
+ if (!parsed || typeof parsed !== "object") {
190
+ throw new HttpError(500, "Invalid response", body);
191
+ }
192
+ return parseDataView(parsed);
193
+ }
194
+ function sleepMs(ms) {
195
+ return new Promise((resolve) => setTimeout(resolve, ms));
196
+ }
197
+ /**
198
+ * Find data views by name. Uses server-side keyword filtering; when `exact` is true,
199
+ * applies client-side `name ===` filter. Optional polling with exponential backoff.
200
+ */
201
+ export async function findDataView(options) {
202
+ const { baseUrl, accessToken, businessDomain = "bd_public", name, datasourceId, exact = false, wait = false, timeoutMs = 30_000, } = options;
203
+ const deadline = Date.now() + timeoutMs;
204
+ let attempt = 0;
205
+ while (true) {
206
+ const list = await listDataViews({
207
+ baseUrl,
208
+ accessToken,
209
+ businessDomain,
210
+ datasourceId,
211
+ name,
212
+ limit: -1,
213
+ });
214
+ const results = exact ? list.filter((v) => v.name === name) : list;
215
+ if (results.length > 0 || !wait || Date.now() >= deadline)
216
+ return results;
217
+ const delayMs = Math.min(5000, 1000 * 2 ** attempt);
218
+ attempt += 1;
219
+ await sleepMs(delayMs);
220
+ }
72
221
  }
@@ -2,7 +2,7 @@ import { type TokenConfig } from "../config/store.js";
2
2
  export declare function normalizeBaseUrl(value: string): string;
3
3
  /**
4
4
  * OAuth2 Authorization Code login flow.
5
- * 1. Register client (if not already registered)
5
+ * 1. Register client (if not already registered), OR use a provided client ID
6
6
  * 2. Open browser to /oauth2/auth
7
7
  * 3. Receive authorization code via local HTTP callback
8
8
  * 4. Exchange code for access_token + refresh_token
@@ -11,6 +11,10 @@ export declare function normalizeBaseUrl(value: string): string;
11
11
  export declare function oauth2Login(baseUrl: string, options?: {
12
12
  port?: number;
13
13
  scope?: string;
14
+ clientId?: string;
15
+ clientSecret?: string;
16
+ /** Skip TLS certificate verification (self-signed / dev servers only). */
17
+ tlsInsecure?: boolean;
14
18
  }): Promise<TokenConfig>;
15
19
  /**
16
20
  * Playwright-automated OAuth2 login.
@@ -28,6 +32,7 @@ export declare function playwrightLogin(baseUrl: string, options?: {
28
32
  password?: string;
29
33
  port?: number;
30
34
  scope?: string;
35
+ tlsInsecure?: boolean;
31
36
  }): Promise<TokenConfig>;
32
37
  /**
33
38
  * Exchange refresh_token for a new access token (OAuth2 password grant style, same as Python ConfigAuth).