11export interface ClientConfig {
22 baseUrl : string ;
33 token ?: string ;
4+ /**
5+ * Custom fetch implementation (e.g. node-fetch or for Next.js caching)
6+ */
7+ fetch ?: ( input : RequestInfo | URL , init ?: RequestInit ) => Promise < Response > ;
48}
59
610export interface DiscoveryResult {
@@ -9,29 +13,48 @@ export interface DiscoveryResult {
913 metadata : string ;
1014 data : string ;
1115 auth : string ;
16+ ui : string ;
1217 } ;
1318 capabilities ?: Record < string , boolean > ;
1419}
1520
21+ export interface QueryOptions {
22+ select ?: string [ ] ;
23+ filters ?: Record < string , any > ;
24+ sort ?: string | string [ ] ; // 'name' or ['-created_at', 'name']
25+ top ?: number ;
26+ skip ?: number ;
27+ }
28+
29+ export interface PaginatedResult < T = any > {
30+ value : T [ ] ;
31+ count : number ;
32+ }
33+
1634export class ObjectStackClient {
1735 private baseUrl : string ;
1836 private token ?: string ;
37+ private fetchImpl : ( input : RequestInfo | URL , init ?: RequestInit ) => Promise < Response > ;
1938 private routes ?: DiscoveryResult [ 'routes' ] ;
2039
2140 constructor ( config : ClientConfig ) {
2241 this . baseUrl = config . baseUrl . replace ( / \/ $ / , '' ) ; // Remove trailing slash
2342 this . token = config . token ;
43+ this . fetchImpl = config . fetch || globalThis . fetch . bind ( globalThis ) ;
2444 }
2545
2646 /**
2747 * Initialize the client by discovering server capabilities and routes.
2848 */
2949 async connect ( ) {
3050 try {
51+ // Connect to the discovery endpoint
52+ // During boot, we might not know routes, so we check convention /api/v1 first
3153 const res = await this . fetch ( `${ this . baseUrl } /api/v1` ) ;
54+
3255 const data = await res . json ( ) ;
3356 this . routes = data . routes ;
34- return data ;
57+ return data as DiscoveryResult ;
3558 } catch ( e ) {
3659 console . error ( 'Failed to connect to ObjectStack Server' , e ) ;
3760 throw e ;
@@ -49,9 +72,8 @@ export class ObjectStackClient {
4972 } ,
5073
5174 getView : async ( object : string , type : 'list' | 'form' = 'list' ) => {
52- // UI routes might not be in discovery map yet, assume convention or add to server
53- // Convention from server/src/index.ts: /api/v1/ui/view/:object
54- const res = await this . fetch ( `${ this . baseUrl } /api/v1/ui/view/${ object } ?type=${ type } ` ) ;
75+ const route = this . getRoute ( 'ui' ) ;
76+ const res = await this . fetch ( `${ this . baseUrl } ${ route } /view/${ object } ?type=${ type } ` ) ;
5577 return res . json ( ) ;
5678 }
5779 } ;
@@ -60,20 +82,37 @@ export class ObjectStackClient {
6082 * Data Operations
6183 */
6284 data = {
63- find : async ( object : string , query : any = { } ) => {
85+ find : async < T = any > ( object : string , options : QueryOptions = { } ) : Promise < PaginatedResult < T > > => {
6486 const route = this . getRoute ( 'data' ) ;
65- const queryString = new URLSearchParams ( query ) . toString ( ) ;
66- const res = await this . fetch ( `${ this . baseUrl } ${ route } /${ object } ?${ queryString } ` ) ;
87+ const queryParams = new URLSearchParams ( ) ;
88+
89+ if ( options . top ) queryParams . set ( 'top' , options . top . toString ( ) ) ;
90+ if ( options . skip ) queryParams . set ( 'skip' , options . skip . toString ( ) ) ;
91+ if ( options . sort ) {
92+ const sortVal = Array . isArray ( options . sort ) ? options . sort . join ( ',' ) : options . sort ;
93+ queryParams . set ( 'sort' , sortVal ) ;
94+ }
95+
96+ // Flatten simple KV pairs if filters exists
97+ if ( options . filters ) {
98+ Object . entries ( options . filters ) . forEach ( ( [ k , v ] ) => {
99+ if ( v !== undefined && v !== null ) {
100+ queryParams . append ( k , String ( v ) ) ;
101+ }
102+ } ) ;
103+ }
104+
105+ const res = await this . fetch ( `${ this . baseUrl } ${ route } /${ object } ?${ queryParams . toString ( ) } ` ) ;
67106 return res . json ( ) ;
68107 } ,
69108
70- get : async ( object : string , id : string ) => {
109+ get : async < T = any > ( object : string , id : string ) : Promise < T > => {
71110 const route = this . getRoute ( 'data' ) ;
72111 const res = await this . fetch ( `${ this . baseUrl } ${ route } /${ object } /${ id } ` ) ;
73112 return res . json ( ) ;
74113 } ,
75114
76- create : async ( object : string , data : any ) => {
115+ create : async < T = any > ( object : string , data : Partial < T > ) : Promise < T > => {
77116 const route = this . getRoute ( 'data' ) ;
78117 const res = await this . fetch ( `${ this . baseUrl } ${ route } /${ object } ` , {
79118 method : 'POST' ,
@@ -82,7 +121,7 @@ export class ObjectStackClient {
82121 return res . json ( ) ;
83122 } ,
84123
85- update : async ( object : string , id : string , data : any ) => {
124+ update : async < T = any > ( object : string , id : string , data : Partial < T > ) : Promise < T > => {
86125 const route = this . getRoute ( 'data' ) ;
87126 const res = await this . fetch ( `${ this . baseUrl } ${ route } /${ object } /${ id } ` , {
88127 method : 'PATCH' ,
@@ -91,7 +130,7 @@ export class ObjectStackClient {
91130 return res . json ( ) ;
92131 } ,
93132
94- delete : async ( object : string , id : string ) => {
133+ delete : async ( object : string , id : string ) : Promise < { success : boolean } > => {
95134 const route = this . getRoute ( 'data' ) ;
96135 const res = await this . fetch ( `${ this . baseUrl } ${ route } /${ object } /${ id } ` , {
97136 method : 'DELETE'
@@ -100,32 +139,41 @@ export class ObjectStackClient {
100139 }
101140 } ;
102141
103- private getRoute ( key : keyof DiscoveryResult [ 'routes' ] ) : string {
104- if ( ! this . routes ) {
105- throw new Error ( 'Client not connected. Call client.connect() first.' ) ;
106- }
107- return this . routes [ key ] ;
108- }
142+ /**
143+ * Private Helpers
144+ */
109145
110- private async fetch ( url : string , options : RequestInit = { } ) {
146+ private async fetch ( url : string , options : RequestInit = { } ) : Promise < Response > {
111147 const headers : Record < string , string > = {
112- 'Content-Type' : 'application/json' ,
113- ...( options . headers as any || { } )
148+ 'Content-Type' : 'application/json' ,
149+ ...( options . headers as Record < string , string > || { } ) ,
114150 } ;
115151
116152 if ( this . token ) {
117153 headers [ 'Authorization' ] = `Bearer ${ this . token } ` ;
118154 }
119155
120- const res = await fetch ( url , {
121- ...options ,
122- headers
123- } ) ;
124-
125- if ( res . status >= 400 ) {
126- throw new Error ( `API Error ${ res . status } : ${ res . statusText } ` ) ;
156+ const res = await this . fetchImpl ( url , { ...options , headers } ) ;
157+
158+ if ( ! res . ok ) {
159+ let errorBody ;
160+ try {
161+ errorBody = await res . json ( ) ;
162+ } catch {
163+ errorBody = { message : res . statusText } ;
164+ }
165+ throw new Error ( `[ObjectStack] Request failed: ${ res . status } ${ JSON . stringify ( errorBody ) } ` ) ;
127166 }
128-
167+
129168 return res ;
130169 }
170+
171+ private getRoute ( key : keyof DiscoveryResult [ 'routes' ] ) : string {
172+ if ( ! this . routes ) {
173+ // Fallback for strictness, but we allow bootstrapping
174+ console . warn ( `[ObjectStackClient] Accessing ${ key } route before connect(). Using default /api/v1/${ key } ` ) ;
175+ return `/api/v1/${ key } ` ;
176+ }
177+ return this . routes [ key ] || `/api/v1/${ key } ` ;
178+ }
131179}
0 commit comments