Skip to content

Commit 92a5885

Browse files
committed
Merge remote-tracking branch 'origin/trunk' into lukim/spice.js
2 parents 998abed + b11ceab commit 92a5885

9 files changed

Lines changed: 171 additions & 34 deletions

File tree

.editorconfig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
[*]
2-
quote_type = single
2+
quote_type = single
3+
trailing_comma = all

.github/actions/setup-vercel-endpoint/action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ runs:
4444
echo "Detected tag/release: ${BRANCH_NAME}, using production endpoint"
4545
else
4646
# For branches, construct the git branch preview URL
47-
# Remove periods, replace / with - and convert to lowercase for Vercel format
48-
BRANCH_SLUG=$(echo "$BRANCH_NAME" | sed 's/\.//g' | sed 's/\//-/g' | tr '[:upper:]' '[:lower:]')
47+
# Remove periods, replace first / with -, remove remaining /, convert to lowercase for Vercel format
48+
BRANCH_SLUG=$(echo "$BRANCH_NAME" | sed 's/\.//g' | sed 's/\//-/' | sed 's/\///g' | tr '[:upper:]' '[:lower:]')
4949
VERCEL_ENDPOINT="https://spice-js-git-${BRANCH_SLUG}-spice.vercel.app"
5050
echo "Detected branch: ${BRANCH_NAME}, using preview endpoint"
5151
fi

.prettierrc.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"singleQuote": true,
3+
"trailingComma": "all",
4+
"printWidth": 80,
5+
"tabWidth": 2,
6+
"semi": true,
7+
"arrowParens": "always"
8+
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Querying data is done through a `SpiceClient` object that initializes the connec
3333
- `apiKey` (string, optional): API key to authenticate with the endpoint.
3434
- `flightUrl` (string, optional): URL of the Flight endpoint to use (default: `localhost:50051`)
3535
- `httpUrl` (string, optional): URL of the HTTP endpoint to use (default: `http://localhost:8090`)
36+
- `logging` (boolean, optional): Enable or disable logging output (default: `true`). Set to `false` to silence all library console output.
3637

3738
Read more about the Spice.ai Apache Arrow Flight API at [docs.spice.ai](https://docs.spice.ai/api/sql-query-api/apache-arrow-flight-api).
3839

src/client-common.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
normalizeSchema,
2525
serializeArrowField,
2626
} from './arrow-utils';
27+
import { Logger } from './logger';
2728

2829
// Retry will be imported by the platform-specific entry point
2930
export interface RetryModule {
@@ -428,6 +429,7 @@ export class SpiceClient {
428429
private _retry: RetryModule;
429430
private _isSpiceCloud: boolean = false;
430431
private _flightOnly: boolean = false;
432+
private _logger: Logger;
431433

432434
// Default Spice Cloud endpoints
433435
private static readonly DEFAULT_CLOUD_HTTP = 'https://data.spiceai.io';
@@ -450,6 +452,7 @@ export class SpiceClient {
450452
this._flightUrl = SpiceClient.DEFAULT_CLOUD_FLIGHT;
451453
this._userAgent = platform.getUserAgent();
452454
this._flightOnly = false;
455+
this._logger = new Logger(true); // Default: logging enabled
453456
} else {
454457
const {
455458
apiKey,
@@ -459,8 +462,12 @@ export class SpiceClient {
459462
userAgent,
460463
customHeaders,
461464
flightOnly,
465+
logging,
462466
} = params;
463467

468+
// Initialize logger (default: enabled)
469+
this._logger = new Logger(logging !== false);
470+
464471
this._apiKey = apiKey;
465472
this._flightOnly = flightOnly || false;
466473

@@ -509,6 +516,7 @@ export class SpiceClient {
509516
this._flightUrl,
510517
this._userAgent,
511518
this._flightTlsEnabled,
519+
this._logger,
512520
);
513521
}
514522

@@ -575,7 +583,7 @@ export class SpiceClient {
575583
);
576584
}
577585

578-
console.debug(configLines.join('\n'));
586+
this._logger.debug(configLines.join('\n'));
579587
}
580588

581589
/**
@@ -803,7 +811,9 @@ export class SpiceClient {
803811
}
804812
}
805813
} catch (parseError) {
806-
console.warn(`[spice.js] Failed to parse JSON line: ${parseError}`);
814+
this._logger.warn(
815+
`[spice.js] Failed to parse JSON line: ${parseError}`,
816+
);
807817
}
808818
}
809819

src/grpc/client.node.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as protobuf from 'protobufjs';
1010
import { EventEmitter } from 'stream';
1111
import { FlightClient, FlightInfo, DescriptorType, Ticket } from '../flight';
1212
import { platform } from '../platform/node';
13+
import { Logger } from '../logger';
1314
// Note: Flight SQL prepared statements are not currently supported by the Spice server.
1415
// The server uses a custom protocol. For parameterized queries, we use client-side substitution.
1516
// This is secure for the supported use cases and matches the HTTP API behavior.
@@ -36,9 +37,9 @@ let grpcAvailable = false;
3637
/**
3738
* Downloads the Flight.proto file from the remote URL and keeps it in memory
3839
*/
39-
async function downloadProtoFile(): Promise<string> {
40+
async function downloadProtoFile(logger?: Logger): Promise<string> {
4041
try {
41-
console.log('[spice.js] Downloading Flight.proto from remote source...');
42+
logger?.info('[spice.js] Downloading Flight.proto from remote source...');
4243
const response = await platform.fetch(PROTO_DOWNLOAD_URL, {
4344
method: 'GET',
4445
headers: {},
@@ -51,11 +52,11 @@ async function downloadProtoFile(): Promise<string> {
5152
}
5253

5354
const content = await response.text();
54-
console.log('[spice.js] Flight.proto downloaded successfully');
55+
logger?.info('[spice.js] Flight.proto downloaded successfully');
5556

5657
return content;
5758
} catch (error: any) {
58-
console.warn(`[spice.js] Failed to download proto file: ${error.message}`);
59+
logger?.warn(`[spice.js] Failed to download proto file: ${error.message}`);
5960
throw error;
6061
}
6162
}
@@ -64,15 +65,15 @@ async function downloadProtoFile(): Promise<string> {
6465
* Loads proto content from local file or downloads it
6566
* Returns the proto content as a string
6667
*/
67-
async function loadProtoContent(): Promise<string | null> {
68+
async function loadProtoContent(logger?: Logger): Promise<string | null> {
6869
// Try local file first
6970
if (fs.existsSync(fullProtoPath)) {
7071
return fs.readFileSync(fullProtoPath, 'utf-8');
7172
}
7273

7374
// Try to download
7475
try {
75-
return await downloadProtoFile();
76+
return await downloadProtoFile(logger);
7677
} catch (error) {
7778
return null;
7879
}
@@ -81,7 +82,7 @@ async function loadProtoContent(): Promise<string | null> {
8182
/**
8283
* Loads proto definition from content in memory using protobufjs
8384
*/
84-
function loadProtoFromContent(content: string): any {
85+
function loadProtoFromContent(content: string, logger?: Logger): any {
8586
try {
8687
// Parse the proto content directly in memory using protobufjs
8788
const root = protobuf.parse(content, { keepCase: false }).root;
@@ -101,7 +102,7 @@ function loadProtoFromContent(content: string): any {
101102
const arrow = grpc.loadPackageDefinition(packageDefinition).arrow as any;
102103
return arrow.flight.protocol;
103104
} catch (error: any) {
104-
console.warn(
105+
logger?.info(
105106
'[spice.js] Failed to load proto from content:',
106107
error.message,
107108
);
@@ -137,17 +138,20 @@ export class GrpcFlightClient {
137138
private flightTlsEnabled: boolean;
138139
private initPromise: Promise<void>;
139140
private useGrpc: boolean = grpcAvailable;
141+
private logger: Logger;
140142

141143
constructor(
142144
apiKey: string | undefined,
143145
flightUrl: string,
144146
userAgent: string,
145147
flightTlsEnabled: boolean,
148+
logger?: Logger,
146149
) {
147150
this.apiKey = apiKey;
148151
this.flightUrl = flightUrl;
149152
this.userAgent = userAgent;
150153
this.flightTlsEnabled = flightTlsEnabled;
154+
this.logger = logger || new Logger(true);
151155
this.initPromise = this.initialize();
152156
}
153157

@@ -163,7 +167,7 @@ export class GrpcFlightClient {
163167
try {
164168
// Check if we already have proto content in memory
165169
if (!protoContent) {
166-
protoContent = await loadProtoContent();
170+
protoContent = await loadProtoContent(this.logger);
167171
}
168172

169173
if (!protoContent) {
@@ -172,7 +176,7 @@ export class GrpcFlightClient {
172176
}
173177

174178
// Load the proto from content
175-
const proto = loadProtoFromContent(protoContent);
179+
const proto = loadProtoFromContent(protoContent, this.logger);
176180

177181
if (!proto?.FlightService) {
178182
throw new Error('Invalid proto file structure');
@@ -182,7 +186,7 @@ export class GrpcFlightClient {
182186
grpcAvailable = true;
183187
this.useGrpc = true;
184188
} catch (error: any) {
185-
console.warn(
189+
this.logger.warn(
186190
`[spice.js] gRPC initialization failed: ${error.message}. Using HTTP endpoint.`,
187191
);
188192
this.useGrpc = false;

src/interfaces.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ export interface SpiceClientConfig {
1111
* @default false
1212
*/
1313
flightOnly?: boolean;
14+
/**
15+
* Enable or disable logging output from the library.
16+
* When false, all console output is suppressed.
17+
* @default true
18+
*/
19+
logging?: boolean;
1420
}
1521

1622
export interface SchemaField {

src/logger.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Simple logger with enable/disable support for spice.js
3+
*
4+
* When disabled, all logging methods are no-ops.
5+
* When enabled, logs are forwarded to the appropriate console methods.
6+
*/
7+
export class Logger {
8+
private readonly enabled: boolean;
9+
10+
constructor(enabled: boolean = true) {
11+
this.enabled = enabled;
12+
}
13+
14+
/**
15+
* Log debug messages (uses console.debug)
16+
*/
17+
debug(...args: unknown[]): void {
18+
if (this.enabled) {
19+
console.debug(...args);
20+
}
21+
}
22+
23+
/**
24+
* Log informational messages (uses console.log)
25+
*/
26+
info(...args: unknown[]): void {
27+
if (this.enabled) {
28+
console.log(...args);
29+
}
30+
}
31+
32+
/**
33+
* Log warning messages (uses console.warn)
34+
*/
35+
warn(...args: unknown[]): void {
36+
if (this.enabled) {
37+
console.warn(...args);
38+
}
39+
}
40+
41+
/**
42+
* Log error messages (uses console.error)
43+
*/
44+
error(...args: unknown[]): void {
45+
if (this.enabled) {
46+
console.error(...args);
47+
}
48+
}
49+
50+
/**
51+
* Log messages (alias for info, uses console.log)
52+
*/
53+
log(...args: unknown[]): void {
54+
if (this.enabled) {
55+
console.log(...args);
56+
}
57+
}
58+
59+
/**
60+
* Check if logging is enabled
61+
*/
62+
isEnabled(): boolean {
63+
return this.enabled;
64+
}
65+
}
66+
67+
/**
68+
* Create a new logger instance
69+
* @param enabled - Whether logging is enabled (default: true)
70+
*/
71+
export function createLogger(enabled: boolean = true): Logger {
72+
return new Logger(enabled);
73+
}

0 commit comments

Comments
 (0)