dremiojs 1.0.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 +69 -19
- package/docs/catalog.md +76 -0
- package/docs/client.md +55 -0
- package/docs/configuration.md +150 -0
- package/docs/governance_collaboration.md +41 -0
- package/docs/provisioning.md +15 -0
- package/docs/reflections.md +54 -0
- package/docs/scripts_lineage_tokens.md +45 -0
- package/docs/service_users.md +77 -0
- package/docs/sources.md +53 -0
- package/docs/sql_and_jobs.md +59 -0
- package/docs/users.md +19 -0
- package/package.json +10 -2
- package/src/api/collaboration.ts +55 -0
- package/src/api/credentials.ts +74 -0
- package/src/api/governance.ts +52 -0
- package/src/api/lineage.ts +19 -0
- package/src/api/provisioning.ts +32 -0
- package/src/api/scripts.ts +64 -0
- package/src/api/token.ts +50 -0
- package/src/api/user.ts +39 -3
- package/src/client/base.ts +25 -0
- package/src/client/cloud.ts +51 -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/integration_manual.ts +49 -0
- package/tests/verify_profiles.ts +28 -0
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
|
+
}
|
|
@@ -39,6 +39,22 @@ const runTests = async () => {
|
|
|
39
39
|
console.warn(' Warning: Could not list reflections:', e.message);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
console.log(' - Validating Provisioning (Engines)...');
|
|
43
|
+
try {
|
|
44
|
+
const engines = await cloudClient.provisioning.listEngines();
|
|
45
|
+
console.log(' Success! Found', engines.length, 'engines');
|
|
46
|
+
} catch (e: any) {
|
|
47
|
+
console.warn(' Warning: Could not list engines:', e.message);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(' - Validating Scripts...');
|
|
51
|
+
try {
|
|
52
|
+
const scripts = await cloudClient.scripts.list();
|
|
53
|
+
console.log(' Success! Found', scripts.length, 'scripts');
|
|
54
|
+
} catch (e: any) {
|
|
55
|
+
console.warn(' Warning: Could not list scripts:', e.message);
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
console.log(' - Running Test Query: SELECT 1');
|
|
43
59
|
const results = await cloudClient.jobs.executeQuery('SELECT 1');
|
|
44
60
|
console.log(' Query Success! Result:', results);
|
|
@@ -80,6 +96,39 @@ const runTests = async () => {
|
|
|
80
96
|
console.warn(' Warning: Could not list reflections:', e.message);
|
|
81
97
|
}
|
|
82
98
|
|
|
99
|
+
console.log(' - Validating Scripts...');
|
|
100
|
+
try {
|
|
101
|
+
const scripts = await softwareClient.scripts.list();
|
|
102
|
+
console.log(' Success! Found', scripts.length, 'scripts');
|
|
103
|
+
} catch (e: any) {
|
|
104
|
+
console.warn(' Warning: Could not list scripts (Check if supported/enabled in this version):', e.message);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(' - Validating Service User Lifecycle...');
|
|
108
|
+
try {
|
|
109
|
+
// 1. Create Service User
|
|
110
|
+
const userName = `test_svc_${Date.now()}`;
|
|
111
|
+
console.log(` Creating Service User: ${userName}...`);
|
|
112
|
+
const newUser = await softwareClient.users.create({
|
|
113
|
+
name: userName,
|
|
114
|
+
identityType: 'SERVICE_USER'
|
|
115
|
+
});
|
|
116
|
+
console.log(' Success! Created User ID:', newUser.id);
|
|
117
|
+
|
|
118
|
+
// 2. Create Credential
|
|
119
|
+
console.log(' Creating Credential...');
|
|
120
|
+
const cred = await softwareClient.credentials.create(newUser.id, 'test-secret', 30);
|
|
121
|
+
console.log(' Success! Created Credential ID:', cred.id);
|
|
122
|
+
|
|
123
|
+
// 3. Delete User
|
|
124
|
+
console.log(' Deleting User...');
|
|
125
|
+
await softwareClient.users.delete(newUser.id, newUser.tag);
|
|
126
|
+
console.log(' Success! User Deleted.');
|
|
127
|
+
|
|
128
|
+
} catch (e: any) {
|
|
129
|
+
console.warn(' Warning: Service User test failed (Admin privileges required?):', e.message);
|
|
130
|
+
}
|
|
131
|
+
|
|
83
132
|
console.log(' - Running Test Query: SELECT 1');
|
|
84
133
|
const results = await softwareClient.jobs.executeQuery('SELECT 1');
|
|
85
134
|
console.log(' Query Success! Result:', results);
|
|
@@ -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();
|