@@ -12,6 +12,8 @@ import type {
1212 ScreenDTO ,
1313 ThemeDTO ,
1414 TranslationDTO ,
15+ ConfigDTO ,
16+ SecretDTO ,
1517} from '../core/dto.js' ;
1618import { EnsembleDocumentType } from '../core/dto.js' ;
1719import { getArtifactConfig , type ArtifactProp } from '../core/artifacts.js' ;
@@ -195,6 +197,8 @@ export type CloudApp = Pick<
195197 | 'theme'
196198 | 'translations'
197199 | 'assets'
200+ | 'config'
201+ | 'secrets'
198202> ;
199203
200204/** Metadata for a saved version (commit); snapshot stored in same doc. */
@@ -773,6 +777,218 @@ function toAssetDTO(doc: FirestoreDocument): AssetDTO {
773777 } ;
774778}
775779
780+ function parseFirestoreMapField (
781+ field : { mapValue ?: { fields ?: Record < string , FirestoreValue > } } | undefined
782+ ) : Record < string , unknown > | undefined {
783+ const mapFields = field ?. mapValue ?. fields ;
784+ if ( ! mapFields || typeof mapFields !== 'object' ) return undefined ;
785+ const result : Record < string , unknown > = { } ;
786+ for ( const [ key , value ] of Object . entries ( mapFields ) ) {
787+ if ( typeof ( value as { stringValue ?: string } ) . stringValue === 'string' ) {
788+ result [ key ] = ( value as { stringValue : string } ) . stringValue ;
789+ continue ;
790+ }
791+ if ( typeof ( value as { booleanValue ?: boolean } ) . booleanValue === 'boolean' ) {
792+ result [ key ] = ( value as { booleanValue : boolean } ) . booleanValue ;
793+ continue ;
794+ }
795+ if ( ( value as { integerValue ?: string } ) . integerValue !== undefined ) {
796+ result [ key ] = Number ( ( value as { integerValue : string } ) . integerValue ) ;
797+ }
798+ }
799+ return Object . keys ( result ) . length > 0 ? result : undefined ;
800+ }
801+
802+ function encodeFirestoreStringMap ( values : Record < string , string > ) : {
803+ mapValue : { fields : Record < string , { stringValue : string } > } ;
804+ } {
805+ const fields : Record < string , { stringValue : string } > = { } ;
806+ for ( const [ key , value ] of Object . entries ( values ) ) {
807+ fields [ key ] = { stringValue : value } ;
808+ }
809+ return { mapValue : { fields } } ;
810+ }
811+
812+ function configDtoToStringMap ( config : ConfigDTO ) : Record < string , string > {
813+ const envVariables = config . envVariables ?? { } ;
814+ const result : Record < string , string > = { } ;
815+ for ( const [ key , value ] of Object . entries ( envVariables ) ) {
816+ if ( value !== undefined && value !== null ) {
817+ result [ key ] = String ( value ) ;
818+ }
819+ }
820+ return result ;
821+ }
822+
823+ function secretsDtoToStringMap ( secrets : SecretDTO ) : Record < string , string > {
824+ const nested =
825+ secrets . secrets && typeof secrets . secrets === 'object'
826+ ? ( secrets . secrets as Record < string , unknown > )
827+ : ( secrets as Record < string , unknown > ) ;
828+ const result : Record < string , string > = { } ;
829+ for ( const [ key , value ] of Object . entries ( nested ) ) {
830+ if ( key === 'secrets' || value === undefined || value === null ) continue ;
831+ result [ key ] = String ( value ) ;
832+ }
833+ return result ;
834+ }
835+
836+ function parseJsonObjectField < T extends object > ( content : string | undefined ) : T | undefined {
837+ if ( ! content ) return undefined ;
838+ try {
839+ const parsed = JSON . parse ( content ) as T ;
840+ return parsed && typeof parsed === 'object' ? parsed : undefined ;
841+ } catch {
842+ return undefined ;
843+ }
844+ }
845+
846+ function toConfigDTO ( doc : FirestoreDocument ) : ConfigDTO | undefined {
847+ const fields = ( doc . fields ?? { } ) as FirestoreFields ;
848+ const fromContent = parseJsonObjectField < ConfigDTO > (
849+ parseFirestoreString ( fields . content as { stringValue ?: string } )
850+ ) ;
851+ const envVariablesFromMap = parseFirestoreMapField (
852+ fields . envVariables as { mapValue ?: { fields ?: Record < string , FirestoreValue > } }
853+ ) ;
854+ const baseUrl = parseFirestoreString ( fields . baseUrl as { stringValue ?: string } ) ;
855+ const useBrowserUrl = parseFirestoreBoolean ( fields . useBrowserUrl as { booleanValue ?: boolean } ) ;
856+ const envVariables =
857+ envVariablesFromMap ?? ( fromContent ?. envVariables as Record < string , unknown > | undefined ) ;
858+ if ( ! envVariables && baseUrl === undefined && useBrowserUrl === undefined ) {
859+ return undefined ;
860+ }
861+ return {
862+ ...( envVariables && { envVariables } ) ,
863+ ...( baseUrl !== undefined && { baseUrl } ) ,
864+ ...( useBrowserUrl !== undefined && { useBrowserUrl } ) ,
865+ } ;
866+ }
867+
868+ function toSecretDTO ( doc : FirestoreDocument ) : SecretDTO | undefined {
869+ const fields = ( doc . fields ?? { } ) as FirestoreFields ;
870+ const fromContent = parseJsonObjectField < SecretDTO > (
871+ parseFirestoreString ( fields . content as { stringValue ?: string } )
872+ ) ;
873+ const secretsFromMap = parseFirestoreMapField (
874+ fields . secrets as { mapValue ?: { fields ?: Record < string , FirestoreValue > } }
875+ ) ;
876+ if ( secretsFromMap ) {
877+ return { secrets : secretsFromMap as Record < string , string > } ;
878+ }
879+ if ( fromContent ) return fromContent ;
880+
881+ const flat = parseFirestoreMapField (
882+ fields as { mapValue ?: { fields ?: Record < string , FirestoreValue > } }
883+ ) ;
884+ return flat ? ( flat as SecretDTO ) : undefined ;
885+ }
886+
887+ async function upsertEnvArtifactDocument (
888+ appId : string ,
889+ idToken : string ,
890+ project : string ,
891+ documentId : string ,
892+ typeValue : string ,
893+ contentJson : string ,
894+ mapFieldName : 'envVariables' | 'secrets' ,
895+ mapValues : Record < string , string > ,
896+ options ?: FirestoreClientOptions
897+ ) : Promise < void > {
898+ const collectionUrl = `https://firestore.googleapis.com/v1/projects/${ project } /databases/(default)/documents/apps/${ appId } /artifacts` ;
899+ const docUrl = `${ collectionUrl } /${ encodeURIComponent ( documentId ) } ` ;
900+ const updatedAt = new Date ( ) . toISOString ( ) ;
901+ const patchFields : FirestoreWriteFields = {
902+ content : { stringValue : contentJson } ,
903+ [ mapFieldName ] : encodeFirestoreStringMap ( mapValues ) ,
904+ updatedAt : { timestampValue : updatedAt } ,
905+ } ;
906+ const fieldPaths = [ 'content' , mapFieldName , 'updatedAt' ] ;
907+
908+ logDebug ( options , {
909+ kind : 'request' ,
910+ method : 'PATCH' ,
911+ url : `${ docUrl } ?${ fieldPaths . map ( ( p ) => `updateMask.fieldPaths=${ encodeURIComponent ( p ) } ` ) . join ( '&' ) } ` ,
912+ context : 'submitEnvDocumentsPush/patch' ,
913+ } ) ;
914+ const patchRes = await fetch (
915+ `${ docUrl } ?${ fieldPaths . map ( ( p ) => `updateMask.fieldPaths=${ encodeURIComponent ( p ) } ` ) . join ( '&' ) } ` ,
916+ {
917+ method : 'PATCH' ,
918+ headers : {
919+ Authorization : `Bearer ${ idToken } ` ,
920+ 'Content-Type' : 'application/json' ,
921+ } ,
922+ body : JSON . stringify ( { fields : patchFields } ) ,
923+ }
924+ ) ;
925+
926+ if ( patchRes . ok ) return ;
927+
928+ if ( patchRes . status !== 404 ) {
929+ throw await toFirestoreError ( `update ${ documentId } ` , patchRes , options ) ;
930+ }
931+
932+ const createUrl = `${ collectionUrl } ?documentId=${ encodeURIComponent ( documentId ) } ` ;
933+ const createFields : FirestoreWriteFields = {
934+ name : { stringValue : documentId } ,
935+ type : { stringValue : typeValue } ,
936+ ...patchFields ,
937+ } ;
938+ logDebug ( options , {
939+ kind : 'request' ,
940+ method : 'POST' ,
941+ url : createUrl ,
942+ context : 'submitEnvDocumentsPush/create' ,
943+ } ) ;
944+ const createRes = await fetch ( createUrl , {
945+ method : 'POST' ,
946+ headers : {
947+ Authorization : `Bearer ${ idToken } ` ,
948+ 'Content-Type' : 'application/json' ,
949+ } ,
950+ body : JSON . stringify ( { fields : createFields } ) ,
951+ } ) ;
952+ if ( ! createRes . ok ) {
953+ throw await toFirestoreError ( `create ${ documentId } ` , createRes , options ) ;
954+ }
955+ }
956+
957+ export async function submitEnvDocumentsPush (
958+ appId : string ,
959+ idToken : string ,
960+ payload : { config ?: ConfigDTO ; secrets ?: SecretDTO } ,
961+ options ?: FirestoreClientOptions
962+ ) : Promise < void > {
963+ const project = getEnsembleFirebaseProject ( ) ;
964+ if ( payload . config ) {
965+ await upsertEnvArtifactDocument (
966+ appId ,
967+ idToken ,
968+ project ,
969+ 'appConfig' ,
970+ EnsembleDocumentType . Environment ,
971+ JSON . stringify ( payload . config ) ,
972+ 'envVariables' ,
973+ configDtoToStringMap ( payload . config ) ,
974+ options
975+ ) ;
976+ }
977+ if ( payload . secrets ) {
978+ await upsertEnvArtifactDocument (
979+ appId ,
980+ idToken ,
981+ project ,
982+ 'secrets' ,
983+ EnsembleDocumentType . Secrets ,
984+ JSON . stringify ( payload . secrets ) ,
985+ 'secrets' ,
986+ secretsDtoToStringMap ( payload . secrets ) ,
987+ options
988+ ) ;
989+ }
990+ }
991+
776992function getCollaboratorRole (
777993 collaboratorsField :
778994 | { mapValue ?: { fields ?: Record < string , { stringValue ?: string } > } }
@@ -1028,13 +1244,17 @@ export async function fetchCloudApp(
10281244 const translations : TranslationDTO [ ] = [ ] ;
10291245 const assets : AssetDTO [ ] = [ ] ;
10301246 let theme : ThemeDTO | undefined ;
1247+ let config : ConfigDTO | undefined ;
1248+ let secrets : SecretDTO | undefined ;
10311249 const i18nDocs : FirestoreDocument [ ] = [ ] ;
10321250 for ( const doc of artifacts ) {
10331251 const docId = getDocId ( doc . name ) ;
10341252 const type = parseFirestoreString ( ( doc . fields ?. type as { stringValue ?: string } ) ?? undefined ) ;
10351253 if ( type === 'screen' ) screens . push ( toScreenDTO ( doc ) ) ;
10361254 else if ( type === 'i18n' ) i18nDocs . push ( doc ) ;
10371255 else if ( type === 'asset' ) assets . push ( toAssetDTO ( doc ) ) ;
1256+ else if ( type === 'config' || docId === 'appConfig' ) config = toConfigDTO ( doc ) ?? config ;
1257+ else if ( type === 'secrets' || docId === 'secrets' ) secrets = toSecretDTO ( doc ) ?? secrets ;
10381258 else if ( type === 'theme' ) {
10391259 if ( ! theme || docId === 'theme' ) {
10401260 theme = toThemeDTO ( doc ) ;
@@ -1065,6 +1285,8 @@ export async function fetchCloudApp(
10651285 ...( theme && { theme } ) ,
10661286 ...( translations . length > 0 && { translations } ) ,
10671287 ...( assets . length > 0 && { assets } ) ,
1288+ ...( config && { config } ) ,
1289+ ...( secrets && { secrets } ) ,
10681290 } ;
10691291}
10701292
0 commit comments