Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/basic/logger-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ async function examplePluginLogging() {
dependencies: ['database'],

init: async (ctx: PluginContext) => {
const db = ctx.getService('db');
const db = ctx.getService('db') as any;
ctx.logger.info('API plugin initialized', { dbConnected: db.connected });

ctx.registerService('api', { server: 'http://localhost:3000' });
Expand Down
3 changes: 2 additions & 1 deletion examples/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@objectstack/spec": "workspace:*"
"@objectstack/spec": "workspace:*",
"@objectstack/core": "workspace:*"
},
"devDependencies": {
"typescript": "^5.0.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"build": "tsc"
},
"dependencies": {
"@objectstack/spec": "workspace:*"
"@objectstack/spec": "workspace:*",
"@objectstack/core": "workspace:*"
},
"devDependencies": {
"typescript": "^5.0.0"
Expand Down
54 changes: 52 additions & 2 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { QueryAST, SortNode, AggregationNode, WindowFunctionNode } from '@objectstack/spec/data';
import { Logger, createLogger } from '@objectstack/core';

export interface ClientConfig {
baseUrl: string;
Expand All @@ -7,6 +8,14 @@ export interface ClientConfig {
* Custom fetch implementation (e.g. node-fetch or for Next.js caching)
*/
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
/**
* Logger instance for debugging
*/
logger?: Logger;
/**
* Enable debug logging
*/
debug?: boolean;
}

export interface DiscoveryResult {
Expand Down Expand Up @@ -41,27 +50,44 @@ export class ObjectStackClient {
private token?: string;
private fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
private routes?: DiscoveryResult['routes'];
private logger: Logger;

constructor(config: ClientConfig) {
this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
this.token = config.token;
this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);

// Initialize logger
this.logger = config.logger || createLogger({
level: config.debug ? 'debug' : 'info',
format: 'pretty'
});

this.logger.debug('ObjectStack client created', { baseUrl: this.baseUrl });
}

/**
* Initialize the client by discovering server capabilities and routes.
*/
async connect() {
this.logger.debug('Connecting to ObjectStack server', { baseUrl: this.baseUrl });

try {
// Connect to the discovery endpoint
// During boot, we might not know routes, so we check convention /api/v1 first
const res = await this.fetch(`${this.baseUrl}/api/v1`);

const data = await res.json();
this.routes = data.routes;

this.logger.info('Connected to ObjectStack server', {
routes: Object.keys(data.routes || {}),
capabilities: data.capabilities
});

return data as DiscoveryResult;
} catch (e) {
console.error('Failed to connect to ObjectStack Server', e);
this.logger.error('Failed to connect to ObjectStack server', e as Error, { baseUrl: this.baseUrl });
throw e;
}
}
Expand Down Expand Up @@ -225,6 +251,12 @@ export class ObjectStackClient {
}

private async fetch(url: string, options: RequestInit = {}): Promise<Response> {
this.logger.debug('HTTP request', {
method: options.method || 'GET',
url,
hasBody: !!options.body
});

const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string> || {}),
Expand All @@ -236,13 +268,28 @@ export class ObjectStackClient {

const res = await this.fetchImpl(url, { ...options, headers });

this.logger.debug('HTTP response', {
method: options.method || 'GET',
url,
status: res.status,
ok: res.ok
});

if (!res.ok) {
let errorBody;
try {
errorBody = await res.json();
} catch {
errorBody = { message: res.statusText };
}

this.logger.error('HTTP request failed', undefined, {
method: options.method || 'GET',
url,
status: res.status,
error: errorBody
});
Comment on lines +286 to +291

Copilot AI Jan 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When logging HTTP errors, it's better to create an actual Error object rather than passing undefined as the error parameter. This would provide better error tracking and stack traces.

Consider:

this.logger.error('HTTP request failed', 
  new Error(`Request failed: ${res.status} ${JSON.stringify(errorBody)}`), 
  { 
    method: options.method || 'GET',
    url,
    status: res.status,
    error: errorBody
  }
);
Suggested change
this.logger.error('HTTP request failed', undefined, {
method: options.method || 'GET',
url,
status: res.status,
error: errorBody
});
this.logger.error(
'HTTP request failed',
new Error(`Request failed: ${res.status} ${JSON.stringify(errorBody)}`),
{
method: options.method || 'GET',
url,
status: res.status,
error: errorBody
}
);

Copilot uses AI. Check for mistakes.

throw new Error(`[ObjectStack] Request failed: ${res.status} ${JSON.stringify(errorBody)}`);
}

Expand All @@ -252,7 +299,10 @@ export class ObjectStackClient {
private getRoute(key: keyof DiscoveryResult['routes']): string {
if (!this.routes) {
// Fallback for strictness, but we allow bootstrapping
console.warn(`[ObjectStackClient] Accessing ${key} route before connect(). Using default /api/v1/${key}`);
this.logger.warn('Accessing route before connect()', {
route: key,
fallback: `/api/v1/${key}`
});
return `/api/v1/${key}`;
}
return this.routes[key] || `/api/v1/${key}`;
Expand Down
Loading
Loading