dremiojs 1.1.0 → 1.2.0
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 +52 -21
- package/docs/configuration.md +129 -33
- package/package.json +6 -2
- package/src/client/cloud.ts +47 -5
- package/src/client/software.ts +77 -33
- package/src/factory.ts +20 -0
- package/src/index.ts +1 -1
- package/src/types/config.ts +28 -5
- package/src/utils/config.ts +113 -0
- package/tests/verify_profiles.ts +28 -0
package/README.md
CHANGED
|
@@ -10,6 +10,49 @@ npm install dremiojs
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
+
The recommended way to use `dremiojs` is by configuring your connection in `~/.dremio/profiles.yaml` (compatible with `dremio-cli`).
|
|
14
|
+
|
|
15
|
+
### 1. Setup Profile
|
|
16
|
+
Create `~/.dremio/profiles.yaml`:
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
profiles:
|
|
20
|
+
cloud:
|
|
21
|
+
type: cloud
|
|
22
|
+
base_url: https://api.dremio.cloud
|
|
23
|
+
project_id: <PROJECT_ID>
|
|
24
|
+
auth:
|
|
25
|
+
type: pat
|
|
26
|
+
token: <PAT>
|
|
27
|
+
local:
|
|
28
|
+
type: software
|
|
29
|
+
base_url: http://localhost:9047
|
|
30
|
+
auth:
|
|
31
|
+
type: username_password
|
|
32
|
+
username: admin
|
|
33
|
+
password: password1
|
|
34
|
+
default_profile: cloud
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Connect & Query
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { Dremio } from 'dremiojs';
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
// Automatically loads 'default' profile from yaml
|
|
44
|
+
const client = await Dremio.fromProfile();
|
|
45
|
+
|
|
46
|
+
// Execute SQL
|
|
47
|
+
const results = await client.jobs.executeQuery('SELECT 1');
|
|
48
|
+
console.log(results);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Advanced Usage (Explicit Configuration)
|
|
53
|
+
|
|
54
|
+
You can also configure clients manually without a profile file.
|
|
55
|
+
|
|
13
56
|
### Dremio Cloud
|
|
14
57
|
|
|
15
58
|
```typescript
|
|
@@ -20,16 +63,6 @@ const client = new DremioCloudClient({
|
|
|
20
63
|
authToken: 'YOUR_PAT',
|
|
21
64
|
projectId: 'YOUR_PROJECT_ID'
|
|
22
65
|
});
|
|
23
|
-
|
|
24
|
-
async function main() {
|
|
25
|
-
// List catalog
|
|
26
|
-
const catalog = await client.catalog.getByPath([]);
|
|
27
|
-
console.log(catalog.children);
|
|
28
|
-
|
|
29
|
-
// Execute SQL
|
|
30
|
-
const results = await client.jobs.executeQuery('SELECT * FROM sys.version');
|
|
31
|
-
console.log(results);
|
|
32
|
-
}
|
|
33
66
|
```
|
|
34
67
|
|
|
35
68
|
### Dremio Software
|
|
@@ -37,29 +70,27 @@ async function main() {
|
|
|
37
70
|
```typescript
|
|
38
71
|
import { DremioSoftwareClient } from 'dremiojs';
|
|
39
72
|
|
|
73
|
+
// OAuth (Client Credentials)
|
|
40
74
|
const client = new DremioSoftwareClient({
|
|
41
|
-
baseUrl: '
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
75
|
+
baseUrl: 'https://dremio.local/api/v3',
|
|
76
|
+
auth: {
|
|
77
|
+
type: 'oauth',
|
|
78
|
+
client_id: 'service-id',
|
|
79
|
+
client_secret: 'secret'
|
|
80
|
+
}
|
|
46
81
|
});
|
|
47
|
-
|
|
48
|
-
async function main() {
|
|
49
|
-
const results = await client.jobs.executeQuery('SELECT 1');
|
|
50
|
-
}
|
|
51
82
|
```
|
|
52
83
|
|
|
53
84
|
## Features
|
|
54
85
|
|
|
86
|
+
- **Profile Support**: Seamless `profiles.yaml` integration for Cloud/Software.
|
|
87
|
+
- **Advanced Auth**: Support for PAT Exchange and OAuth Client Credentials.
|
|
55
88
|
- **Universal Support**: Works with both Dremio Cloud and Dremio Software.
|
|
56
89
|
- **Strict Typing**: Full TypeScript definitions for Catalog, Jobs, Reflections, etc.
|
|
57
90
|
- **SQL Helper**: Easy `executeQuery` method that handles job submission and polling.
|
|
58
91
|
- **Catalog Management**: Create, update, delete Spaces, Sources, Folders, and Datasets.
|
|
59
92
|
- **Reflections**: Manage reflections programmatically.
|
|
60
93
|
|
|
61
|
-
## Documentation
|
|
62
|
-
|
|
63
94
|
- [Configuration & Environment Variables](docs/configuration.md)
|
|
64
95
|
- [Client Usage](docs/client.md)
|
|
65
96
|
- [Catalog API](docs/catalog.md)
|
package/docs/configuration.md
CHANGED
|
@@ -1,54 +1,150 @@
|
|
|
1
|
-
# Configuration &
|
|
1
|
+
# Configuration & Authentication
|
|
2
2
|
|
|
3
|
-
`dremiojs`
|
|
3
|
+
`dremiojs` offers a flexible configuration system that supports:
|
|
4
|
+
1. **Profiles (New):** Automatically load configuration from `~/.dremio/profiles.yaml`.
|
|
5
|
+
2. **Explicit Config:** Pass configuration objects directly to the client constructor.
|
|
6
|
+
3. **Environment Variables:** Fallback support for standard environment variables.
|
|
4
7
|
|
|
5
|
-
##
|
|
8
|
+
## 1. Using Profiles (Recommended)
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
The easiest way to specificy connection details is using the `Dremio` factory, which reads from your local `~/.dremio/profiles.yaml` file (compatible with `dremio-cli` and `dremio-simple-query`).
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
### Installation
|
|
13
|
+
Ensure you have a profiles file at `~/.dremio/profiles.yaml`.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Dremio } from 'dremiojs';
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
// 1. Load the 'default' profile
|
|
20
|
+
const client = await Dremio.fromProfile();
|
|
21
|
+
|
|
22
|
+
// 2. Load a specific named profile (e.g. 'prod')
|
|
23
|
+
const prodClient = await Dremio.fromProfile('prod');
|
|
24
|
+
|
|
25
|
+
const results = await client.jobs.executeQuery('SELECT 1');
|
|
26
|
+
console.log(results);
|
|
27
|
+
}
|
|
16
28
|
```
|
|
17
29
|
|
|
18
|
-
|
|
30
|
+
### Profile Reference (`profiles.yaml`)
|
|
31
|
+
|
|
32
|
+
Below is a complete reference of supported profile types.
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
profiles:
|
|
36
|
+
# --- Dremio Cloud (PAT Auth) ---
|
|
37
|
+
cloud:
|
|
38
|
+
type: cloud
|
|
39
|
+
# Base URL (defaults to https://api.dremio.cloud)
|
|
40
|
+
base_url: https://api.dremio.cloud
|
|
41
|
+
project_id: <YOUR_PROJECT_ID>
|
|
42
|
+
auth:
|
|
43
|
+
type: pat
|
|
44
|
+
token: <YOUR_PAT_TOKEN>
|
|
45
|
+
|
|
46
|
+
# --- Dremio Software (PAT Auth) ---
|
|
47
|
+
software_pat:
|
|
48
|
+
type: software
|
|
49
|
+
base_url: https://dremio.company.com
|
|
50
|
+
auth:
|
|
51
|
+
type: pat
|
|
52
|
+
token: <YOUR_PAT_TOKEN>
|
|
53
|
+
|
|
54
|
+
# --- Dremio Software (Username/Password) ---
|
|
55
|
+
# Useful for local development or legacy auth
|
|
56
|
+
local:
|
|
57
|
+
type: software
|
|
58
|
+
base_url: http://localhost:9047
|
|
59
|
+
ssl: 'false' # Disable SSL verification for localhost
|
|
60
|
+
auth:
|
|
61
|
+
type: username_password
|
|
62
|
+
username: admin
|
|
63
|
+
password: password123
|
|
19
64
|
|
|
20
|
-
|
|
65
|
+
# --- Dremio Software (OAuth 2.0 / Service Account) ---
|
|
66
|
+
# Uses Client Credentials Grant
|
|
67
|
+
service_account:
|
|
68
|
+
type: software
|
|
69
|
+
base_url: https://dremio.company.com
|
|
70
|
+
auth:
|
|
71
|
+
type: oauth
|
|
72
|
+
client_id: <CLIENT_ID>
|
|
73
|
+
client_secret: <CLIENT_SECRET>
|
|
74
|
+
# Scope is optional, defaults to implicit or 'dremio.all' on retry
|
|
75
|
+
scope: dremio.all
|
|
76
|
+
|
|
77
|
+
default_profile: cloud
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 2. Explicit Configuration
|
|
81
|
+
|
|
82
|
+
You can also instantiate clients manually, which is useful for web applications or when configuration is injected at runtime.
|
|
83
|
+
|
|
84
|
+
### Dremio Cloud Client
|
|
21
85
|
|
|
22
86
|
```typescript
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
87
|
+
import { DremioCloudClient } from 'dremiojs';
|
|
88
|
+
|
|
89
|
+
const client = new DremioCloudClient({
|
|
90
|
+
projectId: 'your-project-id',
|
|
91
|
+
authToken: 'your-pat', // Can be a PAT or a Session Token
|
|
92
|
+
baseUrl: 'https://api.dremio.cloud/v0', // Optional
|
|
93
|
+
// Or use the auth object
|
|
94
|
+
auth: {
|
|
95
|
+
type: 'pat',
|
|
96
|
+
token: 'your-pat'
|
|
97
|
+
}
|
|
98
|
+
});
|
|
29
99
|
```
|
|
30
100
|
|
|
31
|
-
### Dremio Software
|
|
101
|
+
### Dremio Software Client
|
|
32
102
|
|
|
33
103
|
```typescript
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
104
|
+
import { DremioSoftwareClient } from 'dremiojs';
|
|
105
|
+
|
|
106
|
+
// PAT Auth
|
|
107
|
+
const client = new DremioSoftwareClient({
|
|
108
|
+
baseUrl: 'http://localhost:9047/api/v3',
|
|
109
|
+
authToken: 'your-pat'
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Username/Password
|
|
113
|
+
const client = new DremioSoftwareClient({
|
|
114
|
+
baseUrl: 'http://localhost:9047/api/v3',
|
|
115
|
+
auth: {
|
|
116
|
+
type: 'username_password',
|
|
117
|
+
username: 'admin',
|
|
118
|
+
password: 'password123'
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// OAuth (Client Credentials)
|
|
123
|
+
const client = new DremioSoftwareClient({
|
|
124
|
+
baseUrl: 'https://dremio.example.com/api/v3',
|
|
125
|
+
auth: {
|
|
126
|
+
type: 'oauth',
|
|
127
|
+
client_id: '...',
|
|
128
|
+
client_secret: '...'
|
|
129
|
+
}
|
|
130
|
+
});
|
|
42
131
|
```
|
|
43
132
|
|
|
44
|
-
##
|
|
133
|
+
## 3. Environment Variables (Fallback)
|
|
134
|
+
|
|
135
|
+
If no profiles are found, `ConfigLoader` checks for these legacy environment variables:
|
|
45
136
|
|
|
46
|
-
|
|
137
|
+
- **Cloud**: `DREMIO_CLOUD_TOKEN`, `DREMIO_CLOUD_PROJECTID`, `DREMIO_CLOUD_BASE_URL`
|
|
138
|
+
- **Software**: `DREMIO_SOFTWARE_TOKEN`, `DREMIO_SOFTWARE_BASE_URL`, `DREMIO_USER`, `DREMIO_PASSWORD`
|
|
139
|
+
|
|
140
|
+
## SSL/TLS Configuration
|
|
141
|
+
|
|
142
|
+
For self-signed certificates (common in local software deployments), set `checkSSLCerts: false`. In `profiles.yaml`, set `ssl: 'false'`.
|
|
47
143
|
|
|
48
144
|
```typescript
|
|
49
145
|
const client = new DremioSoftwareClient({
|
|
50
|
-
baseUrl: 'https://
|
|
51
|
-
|
|
52
|
-
checkSSLCerts: false // WARNING: Use only for development
|
|
146
|
+
baseUrl: 'https://localhost:9047',
|
|
147
|
+
checkSSLCerts: false
|
|
53
148
|
});
|
|
54
149
|
```
|
|
150
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dremiojs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A TypeScript client library for Dremio Cloud and Software.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"type": "commonjs",
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/jest": "^30.0.0",
|
|
19
|
+
"@types/js-yaml": "^4.0.9",
|
|
19
20
|
"@types/node": "^25.0.3",
|
|
20
21
|
"axios": "^1.13.2",
|
|
21
22
|
"dotenv": "^17.2.3",
|
|
@@ -25,5 +26,8 @@
|
|
|
25
26
|
"ts-jest": "^29.4.6",
|
|
26
27
|
"ts-node": "^10.9.2",
|
|
27
28
|
"typescript": "^5.9.3"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"js-yaml": "^4.1.1"
|
|
28
32
|
}
|
|
29
|
-
}
|
|
33
|
+
}
|
package/src/client/cloud.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { BaseClient } from './base';
|
|
2
2
|
import { DremioCloudConfig } from '../types/config';
|
|
3
|
+
import axios from 'axios';
|
|
3
4
|
|
|
4
5
|
export class DremioCloudClient extends BaseClient {
|
|
5
6
|
protected config: DremioCloudConfig;
|
|
7
|
+
private sessionToken: string | null = null;
|
|
6
8
|
|
|
7
9
|
constructor(config: DremioCloudConfig) {
|
|
8
|
-
|
|
9
|
-
throw new Error('Auth token is required for Dremio Cloud.');
|
|
10
|
-
}
|
|
10
|
+
// Validation: Must have Project ID, and EITHER authToken OR auth.type='pat'
|
|
11
11
|
if (!config.projectId) {
|
|
12
12
|
throw new Error('Project ID is required for Dremio Cloud.');
|
|
13
13
|
}
|
|
14
|
+
const hasLegacyToken = !!config.authToken;
|
|
15
|
+
const hasAuthObj = config.auth && config.auth.type === 'pat';
|
|
16
|
+
|
|
17
|
+
if (!hasLegacyToken && !hasAuthObj) {
|
|
18
|
+
throw new Error('Auth token (PAT) is required for Dremio Cloud.');
|
|
19
|
+
}
|
|
14
20
|
|
|
15
21
|
// Adjust BaseUrl to include project context
|
|
22
|
+
// We do this for the main axios instance, but we keep original config for login
|
|
16
23
|
const configWithProject = { ...config };
|
|
17
24
|
if (!configWithProject.baseUrl.includes('/projects/')) {
|
|
18
25
|
// Remove trailing slash if present
|
|
@@ -24,12 +31,47 @@ export class DremioCloudClient extends BaseClient {
|
|
|
24
31
|
this.config = config;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
protected getAuthHeaders(): Record<string, string
|
|
34
|
+
protected async getAuthHeaders(): Promise<Record<string, string>> {
|
|
35
|
+
if (!this.sessionToken) {
|
|
36
|
+
await this.login();
|
|
37
|
+
}
|
|
28
38
|
return {
|
|
29
|
-
Authorization: `Bearer ${this.
|
|
39
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
30
40
|
};
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
private async login(): Promise<void> {
|
|
44
|
+
const pat = this.config.auth?.type === 'pat' ? this.config.auth.token : this.config.authToken;
|
|
45
|
+
|
|
46
|
+
if (!pat) {
|
|
47
|
+
throw new Error('No PAT available for login.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Try to exchange PAT for Session Token
|
|
51
|
+
try {
|
|
52
|
+
// Original Base URL (e.g. https://api.dremio.cloud/v0)
|
|
53
|
+
const cleanBase = this.config.baseUrl.replace(/\/$/, '');
|
|
54
|
+
const loginUrl = `${cleanBase}/login`;
|
|
55
|
+
|
|
56
|
+
const response = await axios.post(loginUrl, { token: pat }, {
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
timeout: this.config.timeout || 10000
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (response.data && response.data.token) {
|
|
62
|
+
this.sessionToken = response.data.token;
|
|
63
|
+
} else {
|
|
64
|
+
// Unexpected response structure
|
|
65
|
+
console.warn('Login response did not contain token, falling back to PAT/Legacy mode.');
|
|
66
|
+
this.sessionToken = pat;
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn('PAT Exchange failed, falling back to raw PAT usage. Error:', (error as any).message);
|
|
70
|
+
// Fallback to using PAT directly
|
|
71
|
+
this.sessionToken = pat;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
33
75
|
// Cloud specific methods can go here or generic methods can use projectId from config
|
|
34
76
|
getProjectId(): string {
|
|
35
77
|
return this.config.projectId;
|
package/src/client/software.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { BaseClient } from './base';
|
|
2
2
|
import { DremioSoftwareConfig } from '../types/config';
|
|
3
|
+
import axios from 'axios';
|
|
3
4
|
|
|
4
5
|
interface LoginResponse {
|
|
5
6
|
token: string;
|
|
6
|
-
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface OAuthResponse {
|
|
10
|
+
access_token: string;
|
|
11
|
+
token_type: string;
|
|
12
|
+
expires_in: number;
|
|
7
13
|
}
|
|
8
14
|
|
|
9
15
|
export class DremioSoftwareClient extends BaseClient {
|
|
@@ -13,8 +19,11 @@ export class DremioSoftwareClient extends BaseClient {
|
|
|
13
19
|
constructor(config: DremioSoftwareConfig) {
|
|
14
20
|
super(config);
|
|
15
21
|
this.config = config;
|
|
22
|
+
// Priority: Explicit authToken -> auth.type='pat' -> null (will login)
|
|
16
23
|
if (config.authToken) {
|
|
17
24
|
this.token = config.authToken;
|
|
25
|
+
} else if (config.auth && config.auth.type === 'pat') {
|
|
26
|
+
this.token = config.auth.token;
|
|
18
27
|
}
|
|
19
28
|
}
|
|
20
29
|
|
|
@@ -27,46 +36,81 @@ export class DremioSoftwareClient extends BaseClient {
|
|
|
27
36
|
};
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
private
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
private getRootUrl(): string {
|
|
40
|
+
// Robustly strip /api/v3 to find the server root
|
|
41
|
+
// Handles: https://dremio.org/api/v3 -> https://dremio.org
|
|
42
|
+
// http://localhost:9047 -> http://localhost:9047
|
|
43
|
+
// http://localhost:9047/ -> http://localhost:9047
|
|
44
|
+
return this.config.baseUrl.replace(/\/api\/v3\/?$/, '').replace(/\/$/, '');
|
|
45
|
+
}
|
|
34
46
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (this.config.baseUrl.endsWith('/api/v3')) {
|
|
39
|
-
const baseUrlRoot = this.config.baseUrl.replace('/api/v3', '');
|
|
40
|
-
// We will perform a direct axios call to the root + /apiv2/login
|
|
41
|
-
// But since our instance is bound to baseUrl, we might need a separate call or specific path
|
|
42
|
-
// Actually, axios instance is bound to /api/v3 usually.
|
|
47
|
+
private async login(): Promise<void> {
|
|
48
|
+
const auth = this.config.auth;
|
|
49
|
+
const rootUrl = this.getRootUrl();
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
if (auth?.type === 'oauth') {
|
|
52
|
+
await this.loginOAuth(rootUrl, auth.client_id, auth.client_secret, auth.scope);
|
|
53
|
+
} else if (auth?.type === 'username_password' || (this.config.username && this.config.password)) {
|
|
54
|
+
const user = auth?.type === 'username_password' ? auth.username : this.config.username;
|
|
55
|
+
const pass = auth?.type === 'username_password' ? auth.password : this.config.password;
|
|
56
|
+
|
|
57
|
+
if (!user || !pass) throw new Error('Username/Password required');
|
|
58
|
+
await this.loginBasic(rootUrl, user, pass);
|
|
59
|
+
} else {
|
|
60
|
+
throw new Error('No valid authentication configuration found (Token, User/Pass, or OAuth).');
|
|
49
61
|
}
|
|
62
|
+
}
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
// or just use absolute URL if we can derive it.
|
|
53
|
-
|
|
54
|
-
// Simplest approach: Assume config.baseUrl is the API base.
|
|
55
|
-
// We'll strip the API part for the login call.
|
|
56
|
-
const apiBase = this.config.baseUrl;
|
|
57
|
-
const rootUrl = apiBase.replace(/\/api\/v3\/?$/, '');
|
|
58
|
-
|
|
64
|
+
private async loginBasic(rootUrl: string, user: string, pass: string): Promise<void> {
|
|
59
65
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
const response = await axios.post<LoginResponse>(`${rootUrl}/apiv2/login`, {
|
|
67
|
+
userName: user,
|
|
68
|
+
password: pass,
|
|
69
|
+
}, {
|
|
70
|
+
timeout: this.config.timeout || 10000
|
|
65
71
|
});
|
|
66
|
-
|
|
67
72
|
this.token = response.data.token;
|
|
68
73
|
} catch (error) {
|
|
69
|
-
console.error('Login failed');
|
|
74
|
+
console.error('Login (Basic) failed:', (error as any).message);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async loginOAuth(rootUrl: string, clientId: string, clientSecret: string, scope?: string): Promise<void> {
|
|
80
|
+
const params = new URLSearchParams();
|
|
81
|
+
params.append('grant_type', 'client_credentials');
|
|
82
|
+
params.append('client_id', clientId);
|
|
83
|
+
params.append('client_secret', clientSecret);
|
|
84
|
+
if (scope) {
|
|
85
|
+
params.append('scope', scope);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const doRequest = async (p: URLSearchParams) => {
|
|
89
|
+
return axios.post<OAuthResponse>(`${rootUrl}/oauth/token`, p, {
|
|
90
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
91
|
+
timeout: this.config.timeout || 10000
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const response = await doRequest(params);
|
|
97
|
+
this.token = response.data.access_token;
|
|
98
|
+
} catch (error: any) {
|
|
99
|
+
// Check for Scope error (400) and retry with dremio.all if not already present
|
|
100
|
+
if (error.response && error.response.status === 400 && !scope) {
|
|
101
|
+
// Heuristic: Check error message content if possible, or just blind retry
|
|
102
|
+
console.warn('OAuth login failed (400). Retrying with scope=dremio.all default...');
|
|
103
|
+
params.set('scope', 'dremio.all');
|
|
104
|
+
try {
|
|
105
|
+
const retryResponse = await doRequest(params);
|
|
106
|
+
this.token = retryResponse.data.access_token;
|
|
107
|
+
return;
|
|
108
|
+
} catch (retryError: any) {
|
|
109
|
+
console.error('OAuth retry failed:', retryError.message);
|
|
110
|
+
throw retryError;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
console.error('OAuth login failed:', error.message);
|
|
70
114
|
throw error;
|
|
71
115
|
}
|
|
72
116
|
}
|
package/src/factory.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ConfigLoader } from './utils/config';
|
|
2
|
+
import { DremioCloudClient } from './client/cloud';
|
|
3
|
+
import { DremioSoftwareClient } from './client/software';
|
|
4
|
+
import { DremioCloudConfig, DremioSoftwareConfig } from './types/config';
|
|
5
|
+
|
|
6
|
+
export class Dremio {
|
|
7
|
+
/**
|
|
8
|
+
* Create a Dremio Client from a named profile in ~/.dremio/profiles.yaml
|
|
9
|
+
* If no profile is specified, it loads the default profile.
|
|
10
|
+
*/
|
|
11
|
+
static async fromProfile(profileName?: string): Promise<DremioCloudClient | DremioSoftwareClient> {
|
|
12
|
+
const config = await ConfigLoader.loadProfile(profileName);
|
|
13
|
+
|
|
14
|
+
if ('projectId' in config) {
|
|
15
|
+
return new DremioCloudClient(config as DremioCloudConfig);
|
|
16
|
+
} else {
|
|
17
|
+
return new DremioSoftwareClient(config as DremioSoftwareConfig);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.ts
CHANGED
package/src/types/config.ts
CHANGED
|
@@ -1,18 +1,41 @@
|
|
|
1
1
|
export interface BaseDremioConfig {
|
|
2
2
|
baseUrl: string;
|
|
3
3
|
timeout?: number;
|
|
4
|
+
checkSSLCerts?: boolean;
|
|
4
5
|
}
|
|
5
6
|
|
|
7
|
+
export type AuthConfig =
|
|
8
|
+
| { type: 'pat'; token: string }
|
|
9
|
+
| { type: 'username_password'; username: string; password: string }
|
|
10
|
+
| { type: 'oauth'; client_id: string; client_secret: string; scope?: string };
|
|
11
|
+
|
|
6
12
|
export interface DremioCloudConfig extends BaseDremioConfig {
|
|
7
|
-
authToken
|
|
13
|
+
authToken?: string; // Legacy/Direct support
|
|
8
14
|
projectId: string;
|
|
15
|
+
auth?: AuthConfig;
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
export interface DremioSoftwareConfig extends BaseDremioConfig {
|
|
12
|
-
authToken?: string;
|
|
13
|
-
username?: string;
|
|
14
|
-
password?: string;
|
|
15
|
-
|
|
19
|
+
authToken?: string; // Legacy/Direct support
|
|
20
|
+
username?: string; // Legacy/Direct support
|
|
21
|
+
password?: string; // Legacy/Direct support
|
|
22
|
+
auth?: AuthConfig;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export type DremioConfig = DremioCloudConfig | DremioSoftwareConfig;
|
|
26
|
+
|
|
27
|
+
// Profile definition matching profiles.yaml
|
|
28
|
+
export interface ProfileConfig {
|
|
29
|
+
type: 'cloud' | 'software';
|
|
30
|
+
base_url?: string;
|
|
31
|
+
project_id?: string;
|
|
32
|
+
test_folder?: string;
|
|
33
|
+
ssl?: string;
|
|
34
|
+
auth: AuthConfig;
|
|
35
|
+
mode?: string; // e.g. 'v25'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ProfilesFile {
|
|
39
|
+
profiles: Record<string, ProfileConfig>;
|
|
40
|
+
default_profile?: string;
|
|
41
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import { ProfilesFile, ProfileConfig, DremioConfig, DremioCloudConfig, DremioSoftwareConfig } from '../types/config';
|
|
6
|
+
|
|
7
|
+
export class ConfigLoader {
|
|
8
|
+
private static PROFILES_PATH = path.join(os.homedir(), '.dremio', 'profiles.yaml');
|
|
9
|
+
|
|
10
|
+
static async loadProfile(profileName?: string): Promise<DremioConfig> {
|
|
11
|
+
let profileConfig: ProfileConfig | undefined;
|
|
12
|
+
|
|
13
|
+
// 1. Try to load from profiles.yaml
|
|
14
|
+
if (fs.existsSync(this.PROFILES_PATH)) {
|
|
15
|
+
try {
|
|
16
|
+
const fileContent = fs.readFileSync(this.PROFILES_PATH, 'utf8');
|
|
17
|
+
const profilesData = yaml.load(fileContent) as ProfilesFile;
|
|
18
|
+
|
|
19
|
+
const targetProfile = profileName || profilesData.default_profile || 'default';
|
|
20
|
+
|
|
21
|
+
if (profilesData.profiles && profilesData.profiles[targetProfile]) {
|
|
22
|
+
profileConfig = profilesData.profiles[targetProfile];
|
|
23
|
+
} else if (profileName) {
|
|
24
|
+
throw new Error(`Profile '${profileName}' not found in ${this.PROFILES_PATH}`);
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.warn(`Warning: Failed to parse ${this.PROFILES_PATH}`, error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Convert to DremioConfig
|
|
32
|
+
if (profileConfig) {
|
|
33
|
+
return this.convertProfileToConfig(profileConfig);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 3. Fallback to Env Vars (legacy support)
|
|
37
|
+
return this.loadFromEnv();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private static convertProfileToConfig(profile: ProfileConfig): DremioConfig {
|
|
41
|
+
const checkSSLCerts = profile.ssl === 'false' ? false : true; // Default true unless 'false' string
|
|
42
|
+
|
|
43
|
+
if (profile.type === 'cloud') {
|
|
44
|
+
if (!profile.project_id) throw new Error('Cloud profile missing project_id');
|
|
45
|
+
// Cloud profile usually has base_url
|
|
46
|
+
let baseUrl = profile.base_url || 'https://api.dremio.cloud';
|
|
47
|
+
|
|
48
|
+
// Normalize: Ensure /v0 assumption for Cloud if not present
|
|
49
|
+
if (!baseUrl.endsWith('/v0')) {
|
|
50
|
+
baseUrl = baseUrl.replace(/\/$/, '');
|
|
51
|
+
baseUrl = `${baseUrl}/v0`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
baseUrl,
|
|
56
|
+
projectId: profile.project_id,
|
|
57
|
+
authToken: profile.auth.type === 'pat' ? profile.auth.token : undefined,
|
|
58
|
+
auth: profile.auth,
|
|
59
|
+
checkSSLCerts
|
|
60
|
+
} as DremioCloudConfig;
|
|
61
|
+
} else {
|
|
62
|
+
// Software
|
|
63
|
+
let baseUrl = profile.base_url || 'http://localhost:9047';
|
|
64
|
+
|
|
65
|
+
// Normalize URL: Ensure it ends with /api/v3 for Software
|
|
66
|
+
if (!baseUrl.endsWith('/api/v3')) {
|
|
67
|
+
// Strip trailing slash first
|
|
68
|
+
baseUrl = baseUrl.replace(/\/$/, '');
|
|
69
|
+
baseUrl = `${baseUrl}/api/v3`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const config: DremioSoftwareConfig = {
|
|
73
|
+
baseUrl,
|
|
74
|
+
checkSSLCerts,
|
|
75
|
+
auth: profile.auth
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Map flat properties if present, though 'auth' object is preferred now
|
|
79
|
+
if (profile.auth.type === 'pat') {
|
|
80
|
+
config.authToken = profile.auth.token;
|
|
81
|
+
} else if (profile.auth.type === 'username_password') {
|
|
82
|
+
config.username = profile.auth.username;
|
|
83
|
+
config.password = profile.auth.password;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return config;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private static loadFromEnv(): DremioConfig {
|
|
91
|
+
// Fallback implementation for existing .env support
|
|
92
|
+
// This mirrors the logic users might expect if no profile exists
|
|
93
|
+
if (process.env.DREMIO_CLOUD_TOKEN && process.env.DREMIO_CLOUD_PROJECTID) {
|
|
94
|
+
return {
|
|
95
|
+
baseUrl: process.env.DREMIO_CLOUD_BASE_URL || 'https://api.dremio.cloud/v0',
|
|
96
|
+
authToken: process.env.DREMIO_CLOUD_TOKEN,
|
|
97
|
+
projectId: process.env.DREMIO_CLOUD_PROJECTID
|
|
98
|
+
} as DremioCloudConfig;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (process.env.DREMIO_SOFTWARE_TOKEN || (process.env.DREMIO_USER && process.env.DREMIO_PASSWORD)) {
|
|
102
|
+
return {
|
|
103
|
+
baseUrl: process.env.DREMIO_SOFTWARE_BASE_URL || 'http://localhost:9047',
|
|
104
|
+
authToken: process.env.DREMIO_SOFTWARE_TOKEN,
|
|
105
|
+
username: process.env.DREMIO_USER,
|
|
106
|
+
password: process.env.DREMIO_PASSWORD,
|
|
107
|
+
checkSSLCerts: process.env.DREMIO_SSL_VERIFY === 'false' ? false : true
|
|
108
|
+
} as DremioSoftwareConfig;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new Error('No configuration found in profiles.yaml or environment variables.');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Dremio } from '../src/factory';
|
|
2
|
+
|
|
3
|
+
async function testProfile(name: string) {
|
|
4
|
+
console.log(`\n--- Testing Profile: ${name} ---`);
|
|
5
|
+
try {
|
|
6
|
+
const client = await Dremio.fromProfile(name);
|
|
7
|
+
console.log(`Initialized client for ${name}. Executing 'SELECT 1'...`);
|
|
8
|
+
|
|
9
|
+
const job = await client.jobs.executeQuery('SELECT 1');
|
|
10
|
+
console.log('Query result:', JSON.stringify(job, null, 2));
|
|
11
|
+
console.log(`✅ [PASS] ${name}`);
|
|
12
|
+
} catch (error: any) {
|
|
13
|
+
console.error(`❌ [FAIL] ${name}`);
|
|
14
|
+
console.error('Error:', error.message);
|
|
15
|
+
if (error.response) {
|
|
16
|
+
console.error('API Response:', error.response.data);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function runTests() {
|
|
22
|
+
await testProfile('cloud');
|
|
23
|
+
await testProfile('software');
|
|
24
|
+
await testProfile('v25');
|
|
25
|
+
await testProfile('service');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
runTests();
|