1+ import { QueryAST , FilterNode , SortNode , AggregationNode , WindowFunctionNode } from '@objectstack/spec' ;
2+
13export interface ClientConfig {
24 baseUrl : string ;
35 token ?: string ;
@@ -19,11 +21,14 @@ export interface DiscoveryResult {
1921}
2022
2123export interface QueryOptions {
22- select ?: string [ ] ;
23- filters ?: Record < string , any > ;
24- sort ?: string | string [ ] ; // 'name' or ['-created_at', 'name']
24+ select ?: string [ ] ; // Simplified Selection
25+ filters ?: Record < string , any > | FilterNode ; // Map or AST
26+ sort ?: string | string [ ] | SortNode [ ] ; // 'name' or ['-created_at'] or AST
2527 top ?: number ;
2628 skip ?: number ;
29+ // Advanced features
30+ aggregations ?: AggregationNode [ ] ;
31+ groupBy ?: string [ ] ;
2732}
2833
2934export interface PaginatedResult < T = any > {
@@ -82,24 +87,66 @@ export class ObjectStackClient {
8287 * Data Operations
8388 */
8489 data = {
90+ /**
91+ * Advanced Query using ObjectStack Query Protocol
92+ * Supports both simplified options and full AST
93+ */
94+ query : async < T = any > ( object : string , query : Partial < QueryAST > ) : Promise < PaginatedResult < T > > => {
95+ const route = this . getRoute ( 'data' ) ;
96+ // POST for complex query to avoid URL length limits and allow clean JSON AST
97+ // Convention: POST /api/v1/data/:object/query
98+ const res = await this . fetch ( `${ this . baseUrl } ${ route } /${ object } /query` , {
99+ method : 'POST' ,
100+ body : JSON . stringify ( query )
101+ } ) ;
102+ return res . json ( ) ;
103+ } ,
104+
85105 find : async < T = any > ( object : string , options : QueryOptions = { } ) : Promise < PaginatedResult < T > > => {
86106 const route = this . getRoute ( 'data' ) ;
87107 const queryParams = new URLSearchParams ( ) ;
88108
109+ // 1. Handle Pagination
89110 if ( options . top ) queryParams . set ( 'top' , options . top . toString ( ) ) ;
90111 if ( options . skip ) queryParams . set ( 'skip' , options . skip . toString ( ) ) ;
112+
113+ // 2. Handle Sort
91114 if ( options . sort ) {
92- const sortVal = Array . isArray ( options . sort ) ? options . sort . join ( ',' ) : options . sort ;
93- queryParams . set ( 'sort' , sortVal ) ;
115+ // Check if it's AST
116+ if ( Array . isArray ( options . sort ) && typeof options . sort [ 0 ] === 'object' ) {
117+ queryParams . set ( 'sort' , JSON . stringify ( options . sort ) ) ;
118+ } else {
119+ const sortVal = Array . isArray ( options . sort ) ? options . sort . join ( ',' ) : options . sort ;
120+ queryParams . set ( 'sort' , sortVal as string ) ;
121+ }
94122 }
95123
96- // Flatten simple KV pairs if filters exists
124+ // 3. Handle Select
125+ if ( options . select ) {
126+ queryParams . set ( 'select' , options . select . join ( ',' ) ) ;
127+ }
128+
129+ // 4. Handle Filters (Simple vs AST)
97130 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- } ) ;
131+ // If looks like AST (not plain object map)
132+ // TODO: robust check. safely assuming map for simplified find, and recommending .query() for AST
133+ if ( this . isFilterAST ( options . filters ) ) {
134+ queryParams . set ( 'filters' , JSON . stringify ( options . filters ) ) ;
135+ } else {
136+ Object . entries ( options . filters ) . forEach ( ( [ k , v ] ) => {
137+ if ( v !== undefined && v !== null ) {
138+ queryParams . append ( k , String ( v ) ) ;
139+ }
140+ } ) ;
141+ }
142+ }
143+
144+ // 5. Handle Aggregations & GroupBy (Pass through as JSON if present)
145+ if ( options . aggregations ) {
146+ queryParams . set ( 'aggregations' , JSON . stringify ( options . aggregations ) ) ;
147+ }
148+ if ( options . groupBy ) {
149+ queryParams . set ( 'groupBy' , options . groupBy . join ( ',' ) ) ;
103150 }
104151
105152 const res = await this . fetch ( `${ this . baseUrl } ${ route } /${ object } ?${ queryParams . toString ( ) } ` ) ;
@@ -121,6 +168,15 @@ export class ObjectStackClient {
121168 return res . json ( ) ;
122169 } ,
123170
171+ createMany : async < T = any > ( object : string , data : Partial < T > [ ] ) : Promise < T [ ] > => {
172+ const route = this . getRoute ( 'data' ) ;
173+ const res = await this . fetch ( `${ this . baseUrl } ${ route } /${ object } /batch` , {
174+ method : 'POST' ,
175+ body : JSON . stringify ( data )
176+ } ) ;
177+ return res . json ( ) ;
178+ } ,
179+
124180 update : async < T = any > ( object : string , id : string , data : Partial < T > ) : Promise < T > => {
125181 const route = this . getRoute ( 'data' ) ;
126182 const res = await this . fetch ( `${ this . baseUrl } ${ route } /${ object } /${ id } ` , {
@@ -130,19 +186,45 @@ export class ObjectStackClient {
130186 return res . json ( ) ;
131187 } ,
132188
189+ updateMany : async < T = any > ( object : string , ids : string [ ] , data : Partial < T > ) : Promise < number > => {
190+ // Warning: This implies updating all IDs with the SAME data
191+ const route = this . getRoute ( 'data' ) ;
192+ const res = await this . fetch ( `${ this . baseUrl } ${ route } /${ object } /batch` , {
193+ method : 'PATCH' ,
194+ body : JSON . stringify ( { ids, data } )
195+ } ) ;
196+ return res . json ( ) ; // Returns count
197+ } ,
198+
133199 delete : async ( object : string , id : string ) : Promise < { success : boolean } > => {
134200 const route = this . getRoute ( 'data' ) ;
135201 const res = await this . fetch ( `${ this . baseUrl } ${ route } /${ object } /${ id } ` , {
136202 method : 'DELETE'
137203 } ) ;
138204 return res . json ( ) ;
205+ } ,
206+
207+ deleteMany : async ( object : string , ids : string [ ] ) : Promise < { count : number } > => {
208+ const route = this . getRoute ( 'data' ) ;
209+ const res = await this . fetch ( `${ this . baseUrl } ${ route } /${ object } /batch` , {
210+ method : 'DELETE' ,
211+ body : JSON . stringify ( { ids } )
212+ } ) ;
213+ return res . json ( ) ;
139214 }
140215 } ;
141216
142217 /**
143218 * Private Helpers
144219 */
145220
221+ private isFilterAST ( filter : any ) : boolean {
222+ // Basic check: if array, it's [field, op, val] or [logic, node, node]
223+ // If object but not basic KV map... harder to tell without schema
224+ // For now, assume if it passes Array.isArray it's an AST root
225+ return Array . isArray ( filter ) ;
226+ }
227+
146228 private async fetch ( url : string , options : RequestInit = { } ) : Promise < Response > {
147229 const headers : Record < string , string > = {
148230 'Content-Type' : 'application/json' ,
0 commit comments