1+ import { URL } from 'url' ;
2+ import * as path from 'path' ;
3+
14import { FirebaseApp } from '../firebase-app' ;
2- import { FirebaseDatabaseError } from '../utils/error' ;
5+ import { FirebaseDatabaseError , AppErrorCodes } from '../utils/error' ;
36import { FirebaseServiceInterface , FirebaseServiceInternalsInterface } from '../firebase-service' ;
47import { Database } from '@firebase/database' ;
58
69import * as validator from '../utils/validator' ;
10+ import { AuthorizedHttpClient , HttpRequestConfig , HttpError } from '../utils/api-request' ;
711
812/**
913 * This variable is redefined in the firebase-js-sdk. Before modifying this
@@ -37,11 +41,19 @@ class DatabaseInternals implements FirebaseServiceInternalsInterface {
3741 }
3842}
3943
44+ declare module '@firebase/database' {
45+ interface Database {
46+ getRules ( ) : Promise < string > ;
47+ getRulesJSON ( ) : Promise < object > ;
48+ setRules ( source : string | Buffer | object ) : Promise < void > ;
49+ }
50+ }
51+
4052export class DatabaseService implements FirebaseServiceInterface {
4153
42- public INTERNAL : DatabaseInternals = new DatabaseInternals ( ) ;
54+ public readonly INTERNAL : DatabaseInternals = new DatabaseInternals ( ) ;
4355
44- private appInternal : FirebaseApp ;
56+ private readonly appInternal : FirebaseApp ;
4557
4658 constructor ( app : FirebaseApp ) {
4759 if ( ! validator . isNonNullObject ( app ) || ! ( 'options' in app ) ) {
@@ -76,6 +88,18 @@ export class DatabaseService implements FirebaseServiceInterface {
7688 const rtdb = require ( '@firebase/database' ) ;
7789 const { version } = require ( '../../package.json' ) ;
7890 db = rtdb . initStandalone ( this . appInternal , dbUrl , version ) . instance ;
91+
92+ const rulesClient = new DatabaseRulesClient ( this . app , dbUrl ) ;
93+ db . getRules = ( ) => {
94+ return rulesClient . getRules ( ) ;
95+ } ;
96+ db . getRulesJSON = ( ) => {
97+ return rulesClient . getRulesJSON ( ) ;
98+ } ;
99+ db . setRules = ( source ) => {
100+ return rulesClient . setRules ( source ) ;
101+ } ;
102+
79103 this . INTERNAL . databases [ dbUrl ] = db ;
80104 }
81105 return db ;
@@ -97,3 +121,122 @@ export class DatabaseService implements FirebaseServiceInterface {
97121 } ) ;
98122 }
99123}
124+
125+ const RULES_URL_PATH = '.settings/rules.json' ;
126+
127+ /**
128+ * A helper client for managing RTDB security rules.
129+ */
130+ class DatabaseRulesClient {
131+
132+ private readonly dbUrl : string ;
133+ private readonly httpClient : AuthorizedHttpClient ;
134+
135+ constructor ( app : FirebaseApp , dbUrl : string ) {
136+ const parsedUrl = new URL ( dbUrl ) ;
137+ parsedUrl . pathname = path . join ( parsedUrl . pathname , RULES_URL_PATH ) ;
138+ this . dbUrl = parsedUrl . toString ( ) ;
139+ this . httpClient = new AuthorizedHttpClient ( app ) ;
140+ }
141+
142+ /**
143+ * Gets the currently applied security rules as a string. The return value consists of
144+ * the rules source including comments.
145+ *
146+ * @return {Promise<string> } A promise fulfilled with the rules as a raw string.
147+ */
148+ public getRules ( ) : Promise < string > {
149+ const req : HttpRequestConfig = {
150+ method : 'GET' ,
151+ url : this . dbUrl ,
152+ } ;
153+ return this . httpClient . send ( req )
154+ . then ( ( resp ) => {
155+ return resp . text ;
156+ } )
157+ . catch ( ( err ) => {
158+ throw this . handleError ( err ) ;
159+ } ) ;
160+ }
161+
162+ /**
163+ * Gets the currently applied security rules as a parsed JSON object. Any comments in
164+ * the original source are stripped away.
165+ *
166+ * @return {Promise<object> } A promise fulfilled with the parsed rules source.
167+ */
168+ public getRulesJSON ( ) : Promise < object > {
169+ const req : HttpRequestConfig = {
170+ method : 'GET' ,
171+ url : this . dbUrl ,
172+ data : { format : 'strict' } ,
173+ } ;
174+ return this . httpClient . send ( req )
175+ . then ( ( resp ) => {
176+ return resp . data ;
177+ } )
178+ . catch ( ( err ) => {
179+ throw this . handleError ( err ) ;
180+ } ) ;
181+ }
182+
183+ /**
184+ * Sets the specified rules on the Firebase Database instance. If the rules source is
185+ * specified as a string or a Buffer, it may include comments.
186+ *
187+ * @param {string|Buffer|object } source Source of the rules to apply. Must not be `null`
188+ * or empty.
189+ * @return {Promise<void> } Resolves when the rules are set on the Database.
190+ */
191+ public setRules ( source : string | Buffer | object ) : Promise < void > {
192+ if ( ! validator . isNonEmptyString ( source ) &&
193+ ! validator . isBuffer ( source ) &&
194+ ! validator . isNonNullObject ( source ) ) {
195+ const error = new FirebaseDatabaseError ( {
196+ code : 'invalid-argument' ,
197+ message : 'Source must be a non-empty string, Buffer or an object.' ,
198+ } ) ;
199+ return Promise . reject ( error ) ;
200+ }
201+
202+ const req : HttpRequestConfig = {
203+ method : 'PUT' ,
204+ url : this . dbUrl ,
205+ data : source ,
206+ headers : {
207+ 'content-type' : 'application/json; charset=utf-8' ,
208+ } ,
209+ } ;
210+ return this . httpClient . send ( req )
211+ . then ( ( ) => {
212+ return ;
213+ } )
214+ . catch ( ( err ) => {
215+ throw this . handleError ( err ) ;
216+ } ) ;
217+ }
218+
219+ private handleError ( err : Error ) : Error {
220+ if ( err instanceof HttpError ) {
221+ return new FirebaseDatabaseError ( {
222+ code : AppErrorCodes . INTERNAL_ERROR ,
223+ message : this . getErrorMessage ( err ) ,
224+ } ) ;
225+ }
226+ return err ;
227+ }
228+
229+ private getErrorMessage ( err : HttpError ) : string {
230+ const intro = 'Error while accessing security rules' ;
231+ try {
232+ const body : { error ?: string } = err . response . data ;
233+ if ( body && body . error ) {
234+ return `${ intro } : ${ body . error . trim ( ) } ` ;
235+ }
236+ } catch {
237+ // Ignore parsing errors
238+ }
239+
240+ return `${ intro } : ${ err . response . text } ` ;
241+ }
242+ }
0 commit comments