1 |
|
2 | "use strict";
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | Object.defineProperty(exports, "__esModule", { value: true });
|
19 | exports.DatabaseService = void 0;
|
20 | const url_1 = require("url");
|
21 | const path = require("path");
|
22 | const error_1 = require("../utils/error");
|
23 | const validator = require("../utils/validator");
|
24 | const api_request_1 = require("../utils/api-request");
|
25 | const index_1 = require("../utils/index");
|
26 | const TOKEN_REFRESH_THRESHOLD_MILLIS = 5 * 60 * 1000;
|
27 | class DatabaseService {
|
28 | constructor(app) {
|
29 | this.databases = {};
|
30 | if (!validator.isNonNullObject(app) || !('options' in app)) {
|
31 | throw new error_1.FirebaseDatabaseError({
|
32 | code: 'invalid-argument',
|
33 | message: 'First argument passed to admin.database() must be a valid Firebase app instance.',
|
34 | });
|
35 | }
|
36 | this.appInternal = app;
|
37 | }
|
38 | get firebaseApp() {
|
39 | return this.app;
|
40 | }
|
41 | |
42 |
|
43 |
|
44 | delete() {
|
45 | if (this.tokenListener) {
|
46 | this.firebaseApp.INTERNAL.removeAuthTokenListener(this.tokenListener);
|
47 | clearTimeout(this.tokenRefreshTimeout);
|
48 | }
|
49 | const promises = [];
|
50 | for (const dbUrl of Object.keys(this.databases)) {
|
51 | const db = this.databases[dbUrl];
|
52 | promises.push(db.INTERNAL.delete());
|
53 | }
|
54 | return Promise.all(promises).then(() => {
|
55 | this.databases = {};
|
56 | });
|
57 | }
|
58 | |
59 |
|
60 |
|
61 |
|
62 |
|
63 | get app() {
|
64 | return this.appInternal;
|
65 | }
|
66 | getDatabase(url) {
|
67 | const dbUrl = this.ensureUrl(url);
|
68 | if (!validator.isNonEmptyString(dbUrl)) {
|
69 | throw new error_1.FirebaseDatabaseError({
|
70 | code: 'invalid-argument',
|
71 | message: 'Database URL must be a valid, non-empty URL string.',
|
72 | });
|
73 | }
|
74 | let db = this.databases[dbUrl];
|
75 | if (typeof db === 'undefined') {
|
76 |
|
77 | const rtdb = require('@firebase/database-compat/standalone');
|
78 | db = rtdb.initStandalone(this.appInternal, dbUrl, (0, index_1.getSdkVersion)()).instance;
|
79 | const rulesClient = new DatabaseRulesClient(this.app, dbUrl);
|
80 | db.getRules = () => {
|
81 | return rulesClient.getRules();
|
82 | };
|
83 | db.getRulesJSON = () => {
|
84 | return rulesClient.getRulesJSON();
|
85 | };
|
86 | db.setRules = (source) => {
|
87 | return rulesClient.setRules(source);
|
88 | };
|
89 | this.databases[dbUrl] = db;
|
90 | }
|
91 | if (!this.tokenListener) {
|
92 | this.tokenListener = this.onTokenChange.bind(this);
|
93 | this.firebaseApp.INTERNAL.addAuthTokenListener(this.tokenListener);
|
94 | }
|
95 | return db;
|
96 | }
|
97 |
|
98 | onTokenChange(_) {
|
99 | const token = this.firebaseApp.INTERNAL.getCachedToken();
|
100 | if (token) {
|
101 | const delayMillis = token.expirationTime - TOKEN_REFRESH_THRESHOLD_MILLIS - Date.now();
|
102 |
|
103 |
|
104 | if (delayMillis > 0) {
|
105 | this.scheduleTokenRefresh(delayMillis);
|
106 | }
|
107 | }
|
108 | }
|
109 | scheduleTokenRefresh(delayMillis) {
|
110 | clearTimeout(this.tokenRefreshTimeout);
|
111 | this.tokenRefreshTimeout = setTimeout(() => {
|
112 | this.firebaseApp.INTERNAL.getToken( true)
|
113 | .catch(() => {
|
114 |
|
115 |
|
116 |
|
117 | });
|
118 | }, delayMillis);
|
119 | }
|
120 | ensureUrl(url) {
|
121 | if (typeof url !== 'undefined') {
|
122 | return url;
|
123 | }
|
124 | else if (typeof this.appInternal.options.databaseURL !== 'undefined') {
|
125 | return this.appInternal.options.databaseURL;
|
126 | }
|
127 | throw new error_1.FirebaseDatabaseError({
|
128 | code: 'invalid-argument',
|
129 | message: 'Can\'t determine Firebase Database URL.',
|
130 | });
|
131 | }
|
132 | }
|
133 | exports.DatabaseService = DatabaseService;
|
134 | const RULES_URL_PATH = '.settings/rules.json';
|
135 |
|
136 |
|
137 |
|
138 | class DatabaseRulesClient {
|
139 | constructor(app, dbUrl) {
|
140 | let parsedUrl = new url_1.URL(dbUrl);
|
141 | const emulatorHost = process.env.FIREBASE_DATABASE_EMULATOR_HOST;
|
142 | if (emulatorHost) {
|
143 | const namespace = extractNamespace(parsedUrl);
|
144 | parsedUrl = new url_1.URL(`http://${emulatorHost}?ns=${namespace}`);
|
145 | }
|
146 | parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH);
|
147 | this.dbUrl = parsedUrl.toString();
|
148 | this.httpClient = new api_request_1.AuthorizedHttpClient(app);
|
149 | }
|
150 | |
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 | getRules() {
|
157 | const req = {
|
158 | method: 'GET',
|
159 | url: this.dbUrl,
|
160 | };
|
161 | return this.httpClient.send(req)
|
162 | .then((resp) => {
|
163 | if (!resp.text) {
|
164 | throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INTERNAL_ERROR, 'HTTP response missing data.');
|
165 | }
|
166 | return resp.text;
|
167 | })
|
168 | .catch((err) => {
|
169 | throw this.handleError(err);
|
170 | });
|
171 | }
|
172 | |
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 | getRulesJSON() {
|
179 | const req = {
|
180 | method: 'GET',
|
181 | url: this.dbUrl,
|
182 | data: { format: 'strict' },
|
183 | };
|
184 | return this.httpClient.send(req)
|
185 | .then((resp) => {
|
186 | return resp.data;
|
187 | })
|
188 | .catch((err) => {
|
189 | throw this.handleError(err);
|
190 | });
|
191 | }
|
192 | |
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 | setRules(source) {
|
201 | if (!validator.isNonEmptyString(source) &&
|
202 | !validator.isBuffer(source) &&
|
203 | !validator.isNonNullObject(source)) {
|
204 | const error = new error_1.FirebaseDatabaseError({
|
205 | code: 'invalid-argument',
|
206 | message: 'Source must be a non-empty string, Buffer or an object.',
|
207 | });
|
208 | return Promise.reject(error);
|
209 | }
|
210 | const req = {
|
211 | method: 'PUT',
|
212 | url: this.dbUrl,
|
213 | data: source,
|
214 | headers: {
|
215 | 'content-type': 'application/json; charset=utf-8',
|
216 | },
|
217 | };
|
218 | return this.httpClient.send(req)
|
219 | .then(() => {
|
220 | return;
|
221 | })
|
222 | .catch((err) => {
|
223 | throw this.handleError(err);
|
224 | });
|
225 | }
|
226 | handleError(err) {
|
227 | if (err instanceof api_request_1.HttpError) {
|
228 | return new error_1.FirebaseDatabaseError({
|
229 | code: error_1.AppErrorCodes.INTERNAL_ERROR,
|
230 | message: this.getErrorMessage(err),
|
231 | });
|
232 | }
|
233 | return err;
|
234 | }
|
235 | getErrorMessage(err) {
|
236 | const intro = 'Error while accessing security rules';
|
237 | try {
|
238 | const body = err.response.data;
|
239 | if (body && body.error) {
|
240 | return `${intro}: ${body.error.trim()}`;
|
241 | }
|
242 | }
|
243 | catch {
|
244 |
|
245 | }
|
246 | return `${intro}: ${err.response.text}`;
|
247 | }
|
248 | }
|
249 | function extractNamespace(parsedUrl) {
|
250 | const ns = parsedUrl.searchParams.get('ns');
|
251 | if (ns) {
|
252 | return ns;
|
253 | }
|
254 | const hostname = parsedUrl.hostname;
|
255 | const dotIndex = hostname.indexOf('.');
|
256 | return hostname.substring(0, dotIndex).toLowerCase();
|
257 | }
|