1
+ import { URL } from 'url' ;
2
+ import * as path from 'path' ;
3
+
1
4
import { FirebaseApp } from '../firebase-app' ;
2
- import { FirebaseDatabaseError } from '../utils/error' ;
5
+ import { FirebaseDatabaseError , AppErrorCodes } from '../utils/error' ;
3
6
import { FirebaseServiceInterface , FirebaseServiceInternalsInterface } from '../firebase-service' ;
4
7
import { Database } from '@firebase/database' ;
5
8
6
9
import * as validator from '../utils/validator' ;
10
+ import { AuthorizedHttpClient , HttpRequestConfig , HttpError } from '../utils/api-request' ;
7
11
8
12
/**
9
13
* This variable is redefined in the firebase-js-sdk. Before modifying this
@@ -37,11 +41,19 @@ class DatabaseInternals implements FirebaseServiceInternalsInterface {
37
41
}
38
42
}
39
43
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
+
40
52
export class DatabaseService implements FirebaseServiceInterface {
41
53
42
- public INTERNAL : DatabaseInternals = new DatabaseInternals ( ) ;
54
+ public readonly INTERNAL : DatabaseInternals = new DatabaseInternals ( ) ;
43
55
44
- private appInternal : FirebaseApp ;
56
+ private readonly appInternal : FirebaseApp ;
45
57
46
58
constructor ( app : FirebaseApp ) {
47
59
if ( ! validator . isNonNullObject ( app ) || ! ( 'options' in app ) ) {
@@ -76,6 +88,18 @@ export class DatabaseService implements FirebaseServiceInterface {
76
88
const rtdb = require ( '@firebase/database' ) ;
77
89
const { version } = require ( '../../package.json' ) ;
78
90
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
+
79
103
this . INTERNAL . databases [ dbUrl ] = db ;
80
104
}
81
105
return db ;
@@ -97,3 +121,122 @@ export class DatabaseService implements FirebaseServiceInterface {
97
121
} ) ;
98
122
}
99
123
}
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