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.
@@ -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
- // Add other fields if necessary
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 async login(): Promise<void> {
31
- if (!this.config.username || !this.config.password) {
32
- throw new Error('Username and password are required for login if no token is provided.');
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
- // Heuristic to handle base URL variations for login
36
- // If base URL is .../api/v3, login is often at .../apiv2/login
37
- let loginUrl = '/apiv2/login';
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
- // Let's rely on the user providing a correct Base URL.
45
- // IMPLEMENTATION NOTE: Dremio Software V3 API usually sits at /api/v3.
46
- // Login is at /apiv2/login.
47
- // We need to construct the login URL relative to the server root, not the V3 API root.
48
- // For simplicity, we will try to infer the root.
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
- // Workaround: create a new axios request for login that overrides the baseURL if necessary
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
- // Direct call using imported axios (not the instance) to avoid prefix issues if needed,
61
- // or just re-config the instance call.
62
- const response = await this.axiosInstance.post<LoginResponse>(`${rootUrl}/apiv2/login`, {
63
- userName: this.config.username,
64
- password: this.config.password,
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
@@ -1,6 +1,6 @@
1
- // Clients
2
1
  export { DremioCloudClient } from './client/cloud';
3
2
  export { DremioSoftwareClient } from './client/software';
3
+ export { Dremio } from './factory';
4
4
 
5
5
  // Types
6
6
  export * from './types/config';
@@ -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: string;
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
- checkSSLCerts?: boolean;
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();