@@ -198,6 +198,203 @@ export class DataConnectApiClient {
198198    const  message  =  error . message  ||  `Unknown server error: ${ response . text }  ; 
199199    return  new  FirebaseDataConnectError ( code ,  message ) ; 
200200  } 
201+ 
202+   /** 
203+    * Converts JSON data into a GraphQL literal string. 
204+    * Handles nested objects, arrays, strings, numbers, and booleans. 
205+    * Ensures strings are properly escaped. 
206+    */ 
207+   private  objectToString ( data : unknown ) : string  { 
208+     if  ( typeof  data  ===  'string' )  { 
209+       const  escapedString  =  data 
210+         . replace ( / \\ / g,  '\\\\' )  // Replace \ with \\ 
211+         . replace ( / " / g,  '\\"' ) ;   // Replace " with \" 
212+       return  `"${ escapedString }  ; 
213+     } 
214+     if  ( typeof  data  ===  'number'  ||  typeof  data  ===  'boolean'  ||  data  ===  null )  { 
215+       return  String ( data ) ; 
216+     } 
217+     if  ( validator . isArray ( data ) )  { 
218+       const  elements  =  data . map ( item  =>  this . objectToString ( item ) ) . join ( ', ' ) ; 
219+       return  `[${ elements }  ; 
220+     } 
221+     if  ( typeof  data  ===  'object'  &&  data  !==  null )  { 
222+       // Filter out properties where the value is undefined BEFORE mapping 
223+       const  kvPairs  =  Object . entries ( data ) 
224+         . filter ( ( [ ,  val ] )  =>  val  !==  undefined ) 
225+         . map ( ( [ key ,  val ] )  =>  { 
226+           // GraphQL object keys are typically unquoted. 
227+           return  `${ key } ${ this . objectToString ( val ) }  ; 
228+         } ) ; 
229+   
230+       if  ( kvPairs . length  ===  0 )  { 
231+         return  '{}' ;  // Represent an object with no defined properties as {} 
232+       } 
233+       return  `{ ${ kvPairs . join ( ', ' ) }  ; 
234+     } 
235+     
236+     // If value is undefined (and not an object property, which is handled above, 
237+     // e.g., if objectToString(undefined) is called directly or for an array element) 
238+     // it should be represented as 'null'. 
239+     if  ( typeof  data  ===  'undefined' )  { 
240+       return  'null' ; 
241+     } 
242+ 
243+     // Fallback for any other types (e.g., Symbol, BigInt - though less common in GQL contexts) 
244+     // Consider how these should be handled or if an error should be thrown. 
245+     // For now, simple string conversion. 
246+     return  String ( data ) ; 
247+   } 
248+ 
249+   private  formatTableName ( tableName : string ) : string  { 
250+     // Format tableName: first character to lowercase 
251+     if  ( tableName  &&  tableName . length  >  0 )  { 
252+       return  tableName . charAt ( 0 ) . toLowerCase ( )  +  tableName . slice ( 1 ) ; 
253+     } 
254+     return  tableName ; 
255+   } 
256+ 
257+   private  handleBulkImportErrors ( err : FirebaseDataConnectError ) : never  { 
258+     if  ( err . code  ===  `data-connect/${ DATA_CONNECT_ERROR_CODE_MAPPING . QUERY_ERROR }  ) { 
259+       throw  new  FirebaseDataConnectError ( 
260+         DATA_CONNECT_ERROR_CODE_MAPPING . QUERY_ERROR , 
261+         `${ err . message }  ) ; 
262+     } 
263+     throw  err ; 
264+   } 
265+ 
266+   /** 
267+    * Insert a single row into the specified table. 
268+    */ 
269+   public  async  insert < GraphQlResponse ,  Variables  extends  object > ( 
270+     tableName : string , 
271+     data : Variables , 
272+   ) : Promise < ExecuteGraphqlResponse < GraphQlResponse > >  { 
273+     if  ( ! validator . isNonEmptyString ( tableName ) )  { 
274+       throw  new  FirebaseDataConnectError ( 
275+         DATA_CONNECT_ERROR_CODE_MAPPING . INVALID_ARGUMENT , 
276+         '`tableName` must be a non-empty string.' ) ; 
277+     } 
278+     if  ( validator . isArray ( data ) )  { 
279+       throw  new  FirebaseDataConnectError ( 
280+         DATA_CONNECT_ERROR_CODE_MAPPING . INVALID_ARGUMENT , 
281+         '`data` must be an object, not an array, for single insert. For arrays, please use `insertMany` function.' ) ; 
282+     } 
283+     if  ( ! validator . isNonNullObject ( data ) )  { 
284+       throw  new  FirebaseDataConnectError ( 
285+         DATA_CONNECT_ERROR_CODE_MAPPING . INVALID_ARGUMENT , 
286+         '`data` must be a non-null object.' ) ; 
287+     } 
288+ 
289+     try  { 
290+       tableName  =  this . formatTableName ( tableName ) ; 
291+       const  gqlDataString  =  this . objectToString ( data ) ; 
292+       const  mutation  =  `mutation { ${ tableName } ${ gqlDataString }  ; 
293+       // Use internal executeGraphql 
294+       return  this . executeGraphql < GraphQlResponse ,  Variables > ( mutation ) . catch ( this . handleBulkImportErrors ) ; 
295+     }  catch  ( e : any )  { 
296+       throw  new  FirebaseDataConnectError ( 
297+         DATA_CONNECT_ERROR_CODE_MAPPING . INTERNAL , 
298+         `Failed to construct insert mutation: ${ e . message }  ) ; 
299+     } 
300+   } 
301+ 
302+   /** 
303+    * Insert multiple rows into the specified table. 
304+    */ 
305+   public  async  insertMany < GraphQlResponse ,  Variables  extends  Array < unknown > > ( 
306+     tableName : string , 
307+     data : Variables , 
308+   ) : Promise < ExecuteGraphqlResponse < GraphQlResponse > >  { 
309+     if  ( ! validator . isNonEmptyString ( tableName ) )  { 
310+       throw  new  FirebaseDataConnectError ( 
311+         DATA_CONNECT_ERROR_CODE_MAPPING . INVALID_ARGUMENT , 
312+         '`tableName` must be a non-empty string.' ) ; 
313+     } 
314+     if  ( ! validator . isNonEmptyArray ( data ) )  { 
315+       throw  new  FirebaseDataConnectError ( 
316+         DATA_CONNECT_ERROR_CODE_MAPPING . INVALID_ARGUMENT , 
317+         '`data` must be a non-empty array for insertMany.' ) ; 
318+     } 
319+ 
320+     try  { 
321+       tableName  =  this . formatTableName ( tableName ) ; 
322+       const  gqlDataString  =  this . objectToString ( data ) ; 
323+       const  mutation  =  `mutation { ${ tableName } ${ gqlDataString }  ; 
324+       // Use internal executeGraphql 
325+       return  this . executeGraphql < GraphQlResponse ,  Variables > ( mutation ) . catch ( this . handleBulkImportErrors ) ; 
326+     }  catch  ( e : any )  { 
327+       throw  new  FirebaseDataConnectError ( DATA_CONNECT_ERROR_CODE_MAPPING . INTERNAL , 
328+         `Failed to construct insertMany mutation: ${ e . message }  ) ; 
329+     } 
330+   } 
331+ 
332+   /** 
333+    * Insert a single row into the specified table, or update it if it already exists. 
334+    */ 
335+   public  async  upsert < GraphQlResponse ,  Variables  extends  object > ( 
336+     tableName : string , 
337+     data : Variables , 
338+   ) : Promise < ExecuteGraphqlResponse < GraphQlResponse > >  { 
339+     if  ( ! validator . isNonEmptyString ( tableName ) )  { 
340+       throw  new  FirebaseDataConnectError ( 
341+         DATA_CONNECT_ERROR_CODE_MAPPING . INVALID_ARGUMENT , 
342+         '`tableName` must be a non-empty string.' ) ; 
343+     } 
344+     if  ( validator . isArray ( data ) )  { 
345+       throw  new  FirebaseDataConnectError ( 
346+         DATA_CONNECT_ERROR_CODE_MAPPING . INVALID_ARGUMENT , 
347+         '`data` must be an object, not an array, for single upsert. For arrays, please use `upsertMany` function.' ) ; 
348+     } 
349+     if  ( ! validator . isNonNullObject ( data ) )  { 
350+       throw  new  FirebaseDataConnectError ( 
351+         DATA_CONNECT_ERROR_CODE_MAPPING . INVALID_ARGUMENT , 
352+         '`data` must be a non-null object.' ) ; 
353+     } 
354+ 
355+     try  { 
356+       tableName  =  this . formatTableName ( tableName ) ; 
357+       const  gqlDataString  =  this . objectToString ( data ) ; 
358+       const  mutation  =  `mutation { ${ tableName } ${ gqlDataString }  ; 
359+       // Use internal executeGraphql 
360+       return  this . executeGraphql < GraphQlResponse ,  Variables > ( mutation ) . catch ( this . handleBulkImportErrors ) ; 
361+     }  catch  ( e : any )  { 
362+       throw  new  FirebaseDataConnectError ( 
363+         DATA_CONNECT_ERROR_CODE_MAPPING . INTERNAL , 
364+         `Failed to construct upsert mutation: ${ e . message }  ) ; 
365+     } 
366+   } 
367+ 
368+   /** 
369+    * Insert multiple rows into the specified table, or update them if they already exist. 
370+    */ 
371+   public  async  upsertMany < GraphQlResponse ,  Variables  extends  Array < unknown > > ( 
372+     tableName : string , 
373+     data : Variables , 
374+   ) : Promise < ExecuteGraphqlResponse < GraphQlResponse > >  { 
375+     if  ( ! validator . isNonEmptyString ( tableName ) )  { 
376+       throw  new  FirebaseDataConnectError ( 
377+         DATA_CONNECT_ERROR_CODE_MAPPING . INVALID_ARGUMENT , 
378+         '`tableName` must be a non-empty string.' ) ; 
379+     } 
380+     if  ( ! validator . isNonEmptyArray ( data ) )  { 
381+       throw  new  FirebaseDataConnectError ( 
382+         DATA_CONNECT_ERROR_CODE_MAPPING . INVALID_ARGUMENT , 
383+         '`data` must be a non-empty array for upsertMany.' ) ; 
384+     } 
385+ 
386+     try  { 
387+       tableName  =  this . formatTableName ( tableName ) ; 
388+       const  gqlDataString  =  this . objectToString ( data ) ; 
389+       const  mutation  =  `mutation { ${ tableName } ${ gqlDataString }  ; 
390+       // Use internal executeGraphql 
391+       return  this . executeGraphql < GraphQlResponse ,  Variables > ( mutation ) . catch ( this . handleBulkImportErrors ) ; 
392+     }  catch  ( e : any )  { 
393+       throw  new  FirebaseDataConnectError ( 
394+         DATA_CONNECT_ERROR_CODE_MAPPING . INTERNAL , 
395+         `Failed to construct upsertMany mutation: ${ e . message }  ) ; 
396+     } 
397+   } 
201398} 
202399
203400/** 
0 commit comments