pyric-admin 0.1.0-alpha.24 → 0.1.0-alpha.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/admin.d.ts +64 -12
- package/dist/admin.d.ts.map +1 -1
- package/dist/admin.js +53 -8
- package/dist/admin.js.map +1 -1
- package/dist/app/index.d.ts +14 -1
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +58 -16
- package/dist/app/index.js.map +1 -1
- package/dist/app-check/index.d.ts +13 -1
- package/dist/app-check/index.d.ts.map +1 -1
- package/dist/app-check/index.js +14 -9
- package/dist/app-check/index.js.map +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +11 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/data-connect/index.d.ts +23 -1
- package/dist/data-connect/index.d.ts.map +1 -1
- package/dist/data-connect/index.js +44 -7
- package/dist/data-connect/index.js.map +1 -1
- package/dist/database/index.d.ts +24 -19
- package/dist/database/index.d.ts.map +1 -1
- package/dist/database/index.js +133 -99
- package/dist/database/index.js.map +1 -1
- package/dist/deferred.d.ts +37 -13
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +57 -15
- package/dist/deferred.js.map +1 -1
- package/dist/eventarc/index.d.ts +13 -1
- package/dist/eventarc/index.d.ts.map +1 -1
- package/dist/eventarc/index.js +15 -9
- package/dist/eventarc/index.js.map +1 -1
- package/dist/extensions/index.d.ts +13 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +15 -9
- package/dist/extensions/index.js.map +1 -1
- package/dist/firestore/index.d.ts +6 -0
- package/dist/firestore/index.d.ts.map +1 -1
- package/dist/firestore/index.js +25 -3
- package/dist/firestore/index.js.map +1 -1
- package/dist/functions/index.d.ts +13 -1
- package/dist/functions/index.d.ts.map +1 -1
- package/dist/functions/index.js +15 -9
- package/dist/functions/index.js.map +1 -1
- package/dist/installations/index.d.ts +13 -1
- package/dist/installations/index.d.ts.map +1 -1
- package/dist/installations/index.js +15 -9
- package/dist/installations/index.js.map +1 -1
- package/dist/instance-id/index.d.ts +13 -1
- package/dist/instance-id/index.d.ts.map +1 -1
- package/dist/instance-id/index.js +15 -9
- package/dist/instance-id/index.js.map +1 -1
- package/dist/machine-learning/index.d.ts +19 -1
- package/dist/machine-learning/index.d.ts.map +1 -1
- package/dist/machine-learning/index.js +23 -9
- package/dist/machine-learning/index.js.map +1 -1
- package/dist/messaging/index.d.ts.map +1 -1
- package/dist/messaging/index.js +1 -0
- package/dist/messaging/index.js.map +1 -1
- package/dist/phone-number-verification/index.d.ts +12 -1
- package/dist/phone-number-verification/index.d.ts.map +1 -1
- package/dist/phone-number-verification/index.js +14 -9
- package/dist/phone-number-verification/index.js.map +1 -1
- package/dist/project-management/index.d.ts +21 -1
- package/dist/project-management/index.d.ts.map +1 -1
- package/dist/project-management/index.js +23 -9
- package/dist/project-management/index.js.map +1 -1
- package/dist/remote-config/index.d.ts +21 -1
- package/dist/remote-config/index.d.ts.map +1 -1
- package/dist/remote-config/index.js +24 -9
- package/dist/remote-config/index.js.map +1 -1
- package/dist/security-rules/index.d.ts +23 -1
- package/dist/security-rules/index.d.ts.map +1 -1
- package/dist/security-rules/index.js +27 -9
- package/dist/security-rules/index.js.map +1 -1
- package/dist/storage/deferred-members.d.ts +14 -0
- package/dist/storage/deferred-members.d.ts.map +1 -0
- package/dist/storage/deferred-members.js +55 -0
- package/dist/storage/deferred-members.js.map +1 -0
- package/dist/storage/delete-files.d.ts +13 -0
- package/dist/storage/delete-files.d.ts.map +1 -0
- package/dist/storage/delete-files.js +39 -0
- package/dist/storage/delete-files.js.map +1 -0
- package/dist/storage/file-query.d.ts +52 -0
- package/dist/storage/file-query.d.ts.map +1 -0
- package/dist/storage/file-query.js +94 -0
- package/dist/storage/file-query.js.map +1 -0
- package/dist/storage/get-files.d.ts +15 -0
- package/dist/storage/get-files.d.ts.map +1 -0
- package/dist/storage/get-files.js +33 -0
- package/dist/storage/get-files.js.map +1 -0
- package/dist/storage/index.d.ts +77 -13
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +359 -160
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/remote-names.d.ts +19 -0
- package/dist/storage/remote-names.d.ts.map +1 -0
- package/dist/storage/remote-names.js +26 -0
- package/dist/storage/remote-names.js.map +1 -0
- package/package.json +2 -2
- package/src/admin.ts +58 -8
- package/src/app/index.ts +71 -19
- package/src/app-check/index.ts +27 -9
- package/src/auth/auth.test.ts +27 -0
- package/src/auth/index.ts +8 -0
- package/src/data-connect/index.ts +70 -9
- package/src/database/database.test.ts +1 -0
- package/src/database/index.ts +182 -108
- package/src/deferred.ts +78 -15
- package/src/eventarc/index.ts +32 -9
- package/src/extensions/index.ts +32 -9
- package/src/firestore/index.ts +30 -3
- package/src/functions/index.ts +32 -9
- package/src/installations/index.ts +33 -10
- package/src/instance-id/index.ts +33 -9
- package/src/machine-learning/index.ts +46 -9
- package/src/messaging/index.ts +1 -0
- package/src/phone-number-verification/index.ts +31 -9
- package/src/project-management/index.ts +48 -8
- package/src/remote-config/index.ts +51 -8
- package/src/security-rules/index.ts +54 -9
- package/src/storage/deferred-members.ts +67 -0
- package/src/storage/delete-files.ts +40 -0
- package/src/storage/file-query.ts +135 -0
- package/src/storage/get-files.ts +40 -0
- package/src/storage/index.ts +484 -180
- package/src/storage/remote-names.ts +34 -0
- package/src/storage/storage.test.ts +78 -5
package/src/database/index.ts
CHANGED
|
@@ -40,11 +40,12 @@
|
|
|
40
40
|
* backing rule state to expose.
|
|
41
41
|
*
|
|
42
42
|
* Sandbox state lives on the underlying `Sandbox` via a `WeakMap`
|
|
43
|
-
* keyed by the `Sandbox` instance
|
|
44
|
-
* the
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
43
|
+
* keyed by the `Sandbox` instance, with one tree per RTDB instance
|
|
44
|
+
* (the instance a database URL names). `sandbox.reset()` wipes every
|
|
45
|
+
* tree via the sandbox's `session_boundary` event.
|
|
46
|
+
* Successive `getDatabase(app)` calls for the same sandbox and instance
|
|
47
|
+
* return the same handle (matches firebase-admin's one database per
|
|
48
|
+
* app and URL).
|
|
48
49
|
*
|
|
49
50
|
* - **Remote sandbox arm** (sandbox target whose `Sandbox` carries the
|
|
50
51
|
* `pyric/sandbox` remote brand — a Node-side handle onto the
|
|
@@ -52,7 +53,9 @@
|
|
|
52
53
|
* `connectRemoteSandbox()`) — every `Reference` data operation routes
|
|
53
54
|
* through the handle's worker-relay channel (`rtdb.get/set/update/
|
|
54
55
|
* remove/push` ops with `actAs: { mode: 'admin' }` pinned — firebase-
|
|
55
|
-
* admin's rules-bypass semantics
|
|
56
|
+
* admin's rules-bypass semantics, and the instance the database
|
|
57
|
+
* URL names on every op and value subscription), NOT into the
|
|
58
|
+
* process-local tree:
|
|
56
59
|
* a local tree on a remote handle would be private server-side data
|
|
57
60
|
* the browser never sees. Differences from the local arm, both
|
|
58
61
|
* deliberate upgrades:
|
|
@@ -78,6 +81,16 @@ import {
|
|
|
78
81
|
type Sandbox,
|
|
79
82
|
} from 'pyric/sandbox';
|
|
80
83
|
|
|
84
|
+
import {
|
|
85
|
+
createDatabaseInstanceRegistry,
|
|
86
|
+
isCustomDatabaseHost,
|
|
87
|
+
isDefaultDatabaseInstance,
|
|
88
|
+
parseDatabaseLocationUrl,
|
|
89
|
+
parseDatabaseUrl,
|
|
90
|
+
type DatabaseInstance,
|
|
91
|
+
type DatabaseInstanceRegistry,
|
|
92
|
+
} from 'pyric/sandbox/internal';
|
|
93
|
+
|
|
81
94
|
import {
|
|
82
95
|
ADMIN_APP_TARGET,
|
|
83
96
|
getApp,
|
|
@@ -85,6 +98,12 @@ import {
|
|
|
85
98
|
} from '../app/index.js';
|
|
86
99
|
import { assertAdminAppActive } from '../app/lifecycle.js';
|
|
87
100
|
import { increment, serverTimestamp } from 'pyric/database';
|
|
101
|
+
import { valueAtChildPath, valueChildren } from 'pyric/database/internal';
|
|
102
|
+
import { deferredExport } from 'pyric/app/internal';
|
|
103
|
+
|
|
104
|
+
/** `firebase-admin/database`'s `enableLogging`. The sandbox does not model
|
|
105
|
+
* the client logger, so it fails with an error naming it. */
|
|
106
|
+
export const enableLogging = deferredExport('pyric-admin/database', 'enableLogging');
|
|
88
107
|
|
|
89
108
|
/** Mirror of `firebase-admin/database`'s `ServerValue` constant and sentinel constructors. */
|
|
90
109
|
export const ServerValue: {
|
|
@@ -154,20 +173,22 @@ type AdminEventType = EventType;
|
|
|
154
173
|
*
|
|
155
174
|
* Signature mirrors `firebase-admin/database`'s `getDatabase(app?)`.
|
|
156
175
|
*
|
|
157
|
-
* - `getDatabase()
|
|
176
|
+
* - `getDatabase()`: default database for the DEFAULT app (resolved
|
|
158
177
|
* through `pyric-admin/app`'s registry, exactly like firebase-admin's
|
|
159
178
|
* no-arg `getDatabase()`; throws `app/no-app` when no default app has
|
|
160
179
|
* been initialized). Works for local and remote sandbox apps.
|
|
161
|
-
* - `getDatabase(app)
|
|
162
|
-
*
|
|
163
|
-
*
|
|
180
|
+
* - `getDatabase(app)`: the database the app's `databaseURL` option names,
|
|
181
|
+
* else the default instance.
|
|
182
|
+
* - `getDatabase(app, url)`: the database `url` names, as
|
|
183
|
+
* {@link getDatabaseWithUrl} selects it.
|
|
164
184
|
*
|
|
165
|
-
*
|
|
166
|
-
* per-`Sandbox` state described in the module-level
|
|
185
|
+
* Each RTDB instance has its own data: one `Database` per sandbox and
|
|
186
|
+
* instance, backed by the per-`Sandbox` state described in the module-level
|
|
187
|
+
* docs. A URL that does not parse as a database URL throws the SDK's error.
|
|
167
188
|
*/
|
|
168
189
|
export function getDatabase(
|
|
169
190
|
app?: PyricAdminApp,
|
|
170
|
-
|
|
191
|
+
url?: string,
|
|
171
192
|
): AdminDatabase {
|
|
172
193
|
if (app === undefined) {
|
|
173
194
|
// No-arg mirror of firebase-admin's `getDatabase()` — resolve the
|
|
@@ -176,7 +197,7 @@ export function getDatabase(
|
|
|
176
197
|
}
|
|
177
198
|
assertAdminAppActive(app);
|
|
178
199
|
if (app[ADMIN_APP_TARGET] === 'sandbox') {
|
|
179
|
-
return getSandboxDatabase(app.sandbox);
|
|
200
|
+
return getSandboxDatabase(app.sandbox, adminDatabaseInstance(app, url));
|
|
180
201
|
}
|
|
181
202
|
throw new TypeError(
|
|
182
203
|
'pyric-admin/database: getDatabase expected a PyricAdminApp ' +
|
|
@@ -185,19 +206,47 @@ export function getDatabase(
|
|
|
185
206
|
}
|
|
186
207
|
|
|
187
208
|
/**
|
|
188
|
-
* Returns the {@link AdminDatabase} service
|
|
189
|
-
* database URL.
|
|
209
|
+
* Returns the {@link AdminDatabase} service for the RTDB instance `url` names.
|
|
190
210
|
*
|
|
191
211
|
* This is the exact `firebase-admin/database` argument order used by the
|
|
192
|
-
* Firebase Functions SDK: `getDatabaseWithUrl(url, app?)`.
|
|
193
|
-
* Functions slice has one shared RTDB instance, so the URL selects that
|
|
194
|
-
* instance rather than creating a second sandbox database.
|
|
212
|
+
* Firebase Functions SDK: `getDatabaseWithUrl(url, app?)`.
|
|
195
213
|
*/
|
|
196
214
|
export function getDatabaseWithUrl(
|
|
197
|
-
|
|
215
|
+
url: string,
|
|
198
216
|
app?: PyricAdminApp,
|
|
199
217
|
): AdminDatabase {
|
|
200
|
-
|
|
218
|
+
if (typeof url !== 'string' || url === '') {
|
|
219
|
+
throw invalidDatabaseUrl();
|
|
220
|
+
}
|
|
221
|
+
return getDatabase(app, url);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** firebase-admin's error for a missing or empty database URL. */
|
|
225
|
+
function invalidDatabaseUrl(): Error {
|
|
226
|
+
return Object.assign(
|
|
227
|
+
new Error('Database URL must be a valid, non-empty URL string.'),
|
|
228
|
+
{ code: 'database/invalid-argument' },
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The instance an admin database targets: `url`, else the app's `databaseURL`
|
|
234
|
+
* option, parsed as the production SDK parses it. `undefined` is the default
|
|
235
|
+
* instance, which the instance named `<projectId>-default-rtdb` or, for a
|
|
236
|
+
* project created before about 2020, `<projectId>` also is when the app's
|
|
237
|
+
* options name the project.
|
|
238
|
+
*/
|
|
239
|
+
function adminDatabaseInstance(
|
|
240
|
+
app: PyricAdminApp,
|
|
241
|
+
url: string | undefined,
|
|
242
|
+
): DatabaseInstance | undefined {
|
|
243
|
+
if (url === '') throw invalidDatabaseUrl();
|
|
244
|
+
const source = url ?? app.options.databaseURL;
|
|
245
|
+
if (source === undefined) return undefined;
|
|
246
|
+
const instance = parseDatabaseUrl(source);
|
|
247
|
+
const projectId = app.options.projectId;
|
|
248
|
+
const isProjectDefault = isDefaultDatabaseInstance(instance.name, projectId);
|
|
249
|
+
return isProjectDefault ? undefined : instance;
|
|
201
250
|
}
|
|
202
251
|
|
|
203
252
|
// ─── Sandbox backend ─────────────────────────────────────────────────
|
|
@@ -218,54 +267,61 @@ type JsonValue =
|
|
|
218
267
|
| { [key: string]: JsonValue };
|
|
219
268
|
|
|
220
269
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* holds onto the `SandboxState` directly so reads / writes don't
|
|
225
|
-
* re-resolve through the WeakMap on every op.
|
|
270
|
+
* One RTDB instance's in-memory tree. The `Database` handle returned to
|
|
271
|
+
* consumers holds onto the `SandboxState` directly so reads / writes don't
|
|
272
|
+
* re-resolve through the registry on every op.
|
|
226
273
|
*/
|
|
227
274
|
interface SandboxState {
|
|
228
275
|
root: Record<string, JsonValue>;
|
|
229
276
|
}
|
|
230
277
|
|
|
231
|
-
/** One
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
278
|
+
/** One instance's tree and the one `Database` handle over it, built on first use. */
|
|
279
|
+
interface LocalInstance {
|
|
280
|
+
state: SandboxState;
|
|
281
|
+
db?: AdminDatabase;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** One registry of instances per `Sandbox`. Successive `getDatabase(app)`
|
|
285
|
+
* calls for the same sandbox and instance return the same handle, and
|
|
286
|
+
* each instance keeps its own tree, as firebase-admin keeps one database
|
|
287
|
+
* per URL. */
|
|
288
|
+
const localInstancesBySandbox = new WeakMap<Sandbox, DatabaseInstanceRegistry<LocalInstance>>();
|
|
289
|
+
|
|
290
|
+
function localInstances(sandbox: Sandbox): DatabaseInstanceRegistry<LocalInstance> {
|
|
291
|
+
const existing = localInstancesBySandbox.get(sandbox);
|
|
292
|
+
if (existing !== undefined) return existing;
|
|
293
|
+
const registry: DatabaseInstanceRegistry<LocalInstance> = createDatabaseInstanceRegistry({
|
|
294
|
+
create: () => ({ state: { root: {} } }),
|
|
295
|
+
});
|
|
296
|
+
localInstancesBySandbox.set(sandbox, registry);
|
|
297
|
+
// Wire `sandbox.reset()` → wipe every instance's tree. `session_boundary`
|
|
298
|
+
// fires before the env swap, so consumer code that observes a reset sees
|
|
243
299
|
// the freshly-cleared tree on the next read. `dispose` also fires a
|
|
244
300
|
// boundary; treat it the same (the sandbox is being torn down — any
|
|
245
301
|
// in-flight handle on the tree gets an empty view).
|
|
246
302
|
sandbox.onEvent((event) => {
|
|
247
303
|
if (event.kind === 'session_boundary') {
|
|
248
|
-
state
|
|
304
|
+
for (const [, entry] of registry.entries()) entry.state.root = {};
|
|
249
305
|
}
|
|
250
306
|
});
|
|
251
|
-
return
|
|
307
|
+
return registry;
|
|
252
308
|
}
|
|
253
309
|
|
|
254
|
-
/** Build (or reuse) the sandbox Database handle for `sandbox
|
|
310
|
+
/** Build (or reuse) the sandbox Database handle for `sandbox` and `instance`
|
|
311
|
+
* (`undefined` is the default instance).
|
|
255
312
|
*
|
|
256
313
|
* REMOTE handles dispatch here, BEFORE any local state is touched: a
|
|
257
314
|
* remote sandbox must never get a `SandboxState` (a private local tree)
|
|
258
315
|
* or a `sandbox.onEvent` wire-up (which throws on remote handles). */
|
|
259
|
-
function getSandboxDatabase(sandbox: Sandbox): AdminDatabase {
|
|
316
|
+
function getSandboxDatabase(sandbox: Sandbox, instance: DatabaseInstance | undefined): AdminDatabase {
|
|
260
317
|
if (isRemoteSandbox(sandbox)) {
|
|
261
|
-
return getRemoteDatabase(sandbox);
|
|
318
|
+
return getRemoteDatabase(sandbox, instance);
|
|
262
319
|
}
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return buildDatabaseShell((db, path) => buildSandboxRef(state, db, path));
|
|
320
|
+
const registry = localInstances(sandbox);
|
|
321
|
+
const entry = registry.getOrCreate(registry.keyOf(instance));
|
|
322
|
+
const state = entry.state;
|
|
323
|
+
entry.db ??= buildDatabaseShell(instance, (handle, path) => buildSandboxRef(state, handle, path));
|
|
324
|
+
return entry.db;
|
|
269
325
|
}
|
|
270
326
|
|
|
271
327
|
/**
|
|
@@ -275,18 +331,26 @@ function buildSandboxDatabase(state: SandboxState): AdminDatabase {
|
|
|
275
331
|
* local emulator).
|
|
276
332
|
*/
|
|
277
333
|
function buildDatabaseShell(
|
|
334
|
+
instance: DatabaseInstance | undefined,
|
|
278
335
|
refFactory: (db: AdminDatabase, path: string) => AdminReference,
|
|
279
336
|
): AdminDatabase {
|
|
280
337
|
const db = {
|
|
281
338
|
ref(path?: string): AdminReference {
|
|
282
339
|
return refFactory(db as unknown as AdminDatabase, path ?? '/');
|
|
283
340
|
},
|
|
341
|
+
/** As the SDK parses it: the URL names a location, and for a
|
|
342
|
+
* `firebaseio.com` database its host must be this database's host. */
|
|
284
343
|
refFromURL(url: string): AdminReference {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
344
|
+
const location = parseDatabaseLocationUrl(url);
|
|
345
|
+
if (instance !== undefined) {
|
|
346
|
+
const expectedHost = parseDatabaseLocationUrl(instance.url).host;
|
|
347
|
+
if (!isCustomDatabaseHost(expectedHost) && location.host !== expectedHost) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`FIREBASE FATAL ERROR: refFromURL: Host name does not match the current database: (found ${location.host} but expected ${expectedHost}) `,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return refFactory(db as unknown as AdminDatabase, location.path);
|
|
290
354
|
},
|
|
291
355
|
// Admin-only metadata methods — not modeled in the sandbox. The
|
|
292
356
|
// sandbox is rule-bypass by construction; surfacing rule JSON would
|
|
@@ -757,6 +821,9 @@ function buildSandboxSnap(
|
|
|
757
821
|
const segs = pathSegments(path);
|
|
758
822
|
const key = segs.length === 0 ? null : segs[segs.length - 1]!;
|
|
759
823
|
const exists = val !== null;
|
|
824
|
+
// An array value still has children: `val()` renders integer keys as an
|
|
825
|
+
// array, and the SDK iterates them as '0', '1', ... (#1110).
|
|
826
|
+
const children = valueChildren(val);
|
|
760
827
|
const snap = {
|
|
761
828
|
key,
|
|
762
829
|
get ref(): AdminReference {
|
|
@@ -770,35 +837,21 @@ function buildSandboxSnap(
|
|
|
770
837
|
},
|
|
771
838
|
child(p: string): AdminDataSnapshot {
|
|
772
839
|
const childSegs = pathSegments(p);
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
if (cur === null || typeof cur !== 'object' || Array.isArray(cur)) {
|
|
776
|
-
cur = null;
|
|
777
|
-
break;
|
|
778
|
-
}
|
|
779
|
-
cur = (cur as Record<string, JsonValue>)[s] ?? null;
|
|
780
|
-
}
|
|
781
|
-
return buildSandboxSnap(refAt, joinPath([...segs, ...childSegs]), cur);
|
|
840
|
+
const childVal = valueAtChildPath(val, childSegs) as JsonValue;
|
|
841
|
+
return buildSandboxSnap(refAt, joinPath([...segs, ...childSegs]), childVal);
|
|
782
842
|
},
|
|
783
843
|
hasChild(p: string): boolean {
|
|
784
844
|
return snap.child(p).exists();
|
|
785
845
|
},
|
|
786
846
|
hasChildren(): boolean {
|
|
787
|
-
return
|
|
788
|
-
val !== null &&
|
|
789
|
-
typeof val === 'object' &&
|
|
790
|
-
!Array.isArray(val) &&
|
|
791
|
-
Object.keys(val as Record<string, JsonValue>).length > 0
|
|
792
|
-
);
|
|
847
|
+
return children.length > 0;
|
|
793
848
|
},
|
|
794
849
|
numChildren(): number {
|
|
795
|
-
|
|
796
|
-
return Object.keys(val as Record<string, JsonValue>).length;
|
|
850
|
+
return children.length;
|
|
797
851
|
},
|
|
798
852
|
forEach(cb: (child: AdminDataSnapshot) => boolean | void): boolean {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
const childSnap = buildSandboxSnap(refAt, joinPath([...segs, k]), v);
|
|
853
|
+
for (const { key: childKey, value } of children) {
|
|
854
|
+
const childSnap = buildSandboxSnap(refAt, joinPath([...segs, childKey]), value as JsonValue);
|
|
802
855
|
if (cb(childSnap) === true) return true;
|
|
803
856
|
}
|
|
804
857
|
return false;
|
|
@@ -845,30 +898,60 @@ interface RemoteWireSnapshot {
|
|
|
845
898
|
}
|
|
846
899
|
|
|
847
900
|
/**
|
|
848
|
-
* Per-remote-handle state: the relay channel
|
|
849
|
-
*
|
|
901
|
+
* Per-remote-handle, per-instance state: the relay channel, the instance
|
|
902
|
+
* every op and subscription names (`undefined` is the default instance),
|
|
903
|
+
* and the `on('value')` listener registry (`path → callback → detach`) that
|
|
904
|
+
* `off()` consults.
|
|
850
905
|
*/
|
|
851
906
|
interface RemoteDbState {
|
|
852
907
|
channel: RemoteSandboxChannel;
|
|
908
|
+
instance: string | undefined;
|
|
853
909
|
listeners: Map<string, Map<unknown, () => void>>;
|
|
854
910
|
}
|
|
855
911
|
|
|
856
|
-
/** One `Database` per remote handle
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
* in the
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
function getRemoteDatabase(sandbox: RemoteSandbox): AdminDatabase {
|
|
863
|
-
let
|
|
864
|
-
if (
|
|
912
|
+
/** One `Database` per remote handle and instance. Successive
|
|
913
|
+
* `getDatabase(app)` calls for an instance share the listener registry
|
|
914
|
+
* (matches the local arm's singleton-per-instance semantics). Keyed off
|
|
915
|
+
* the handle object; the data itself lives in the host. */
|
|
916
|
+
const remoteDbsBySandbox = new WeakMap<Sandbox, DatabaseInstanceRegistry<{ db?: AdminDatabase }>>();
|
|
917
|
+
|
|
918
|
+
function getRemoteDatabase(sandbox: RemoteSandbox, instance: DatabaseInstance | undefined): AdminDatabase {
|
|
919
|
+
let registry = remoteDbsBySandbox.get(sandbox);
|
|
920
|
+
if (registry === undefined) {
|
|
921
|
+
registry = createDatabaseInstanceRegistry({ create: () => ({}) });
|
|
922
|
+
remoteDbsBySandbox.set(sandbox, registry);
|
|
923
|
+
}
|
|
924
|
+
const entry = registry.getOrCreate(registry.keyOf(instance));
|
|
925
|
+
if (entry.db !== undefined) return entry.db;
|
|
865
926
|
const state: RemoteDbState = {
|
|
866
927
|
channel: sandbox.channel,
|
|
928
|
+
instance: instance?.name,
|
|
867
929
|
listeners: new Map(),
|
|
868
930
|
};
|
|
869
|
-
db = buildDatabaseShell((dbHandle, path) => buildRemoteRef(state, dbHandle, path));
|
|
870
|
-
|
|
871
|
-
|
|
931
|
+
entry.db = buildDatabaseShell(instance, (dbHandle, path) => buildRemoteRef(state, dbHandle, path));
|
|
932
|
+
return entry.db;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** An RTDB op this arm relays, without the instance and lens {@link relayRtdbOp} adds. */
|
|
936
|
+
type RemoteRtdbOp = { method: `rtdb.${string}`; path: string } & Record<string, unknown>;
|
|
937
|
+
|
|
938
|
+
/** Relay `op` on the state's instance, under firebase-admin's rules-bypass lens. */
|
|
939
|
+
function relayRtdbOp(state: RemoteDbState, op: RemoteRtdbOp): Promise<unknown> {
|
|
940
|
+
return state.channel.op({ ...op, instance: state.instance, actAs: REMOTE_ADMIN_LENS });
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** Subscribe to the value at `path` on the state's instance, under the admin lens. */
|
|
944
|
+
function subscribeRtdbValue(
|
|
945
|
+
state: RemoteDbState,
|
|
946
|
+
path: string,
|
|
947
|
+
onSnap: (value: unknown) => void,
|
|
948
|
+
onError: (err: Error & { code: string }) => void,
|
|
949
|
+
): () => void {
|
|
950
|
+
return state.channel.subscribe(
|
|
951
|
+
{ target: { service: 'rtdb', instance: state.instance, path }, actAs: REMOTE_ADMIN_LENS },
|
|
952
|
+
onSnap,
|
|
953
|
+
onError,
|
|
954
|
+
);
|
|
872
955
|
}
|
|
873
956
|
|
|
874
957
|
/**
|
|
@@ -910,20 +993,18 @@ function buildRemoteRef(
|
|
|
910
993
|
|
|
911
994
|
/** Set `value` at this path. `null` deletes. Relays `rtdb.set`. */
|
|
912
995
|
async set(value: unknown): Promise<void> {
|
|
913
|
-
await state
|
|
996
|
+
await relayRtdbOp(state, {
|
|
914
997
|
method: 'rtdb.set',
|
|
915
998
|
path: canonical,
|
|
916
999
|
value: value ?? null,
|
|
917
|
-
actAs: REMOTE_ADMIN_LENS,
|
|
918
1000
|
});
|
|
919
1001
|
},
|
|
920
1002
|
|
|
921
1003
|
/** Read this path (`rtdb.get`). Resolves to a `DataSnapshot`. */
|
|
922
1004
|
async get(): Promise<AdminDataSnapshot> {
|
|
923
|
-
const wire = (await state
|
|
1005
|
+
const wire = (await relayRtdbOp(state, {
|
|
924
1006
|
method: 'rtdb.get',
|
|
925
1007
|
path: canonical,
|
|
926
|
-
actAs: REMOTE_ADMIN_LENS,
|
|
927
1008
|
})) as RemoteWireSnapshot;
|
|
928
1009
|
return snapFromWire(wire);
|
|
929
1010
|
},
|
|
@@ -943,8 +1024,9 @@ function buildRemoteRef(
|
|
|
943
1024
|
return new Promise<AdminDataSnapshot>((resolve, reject) => {
|
|
944
1025
|
let detach: (() => void) | null = null;
|
|
945
1026
|
let settled = false;
|
|
946
|
-
detach =
|
|
947
|
-
|
|
1027
|
+
detach = subscribeRtdbValue(
|
|
1028
|
+
state,
|
|
1029
|
+
canonical,
|
|
948
1030
|
(value) => {
|
|
949
1031
|
if (settled) return;
|
|
950
1032
|
settled = true;
|
|
@@ -971,20 +1053,18 @@ function buildRemoteRef(
|
|
|
971
1053
|
'pyric-admin/database sandbox: update expected an object.',
|
|
972
1054
|
);
|
|
973
1055
|
}
|
|
974
|
-
await state
|
|
1056
|
+
await relayRtdbOp(state, {
|
|
975
1057
|
method: 'rtdb.update',
|
|
976
1058
|
path: canonical,
|
|
977
1059
|
values: values as Record<string, unknown>,
|
|
978
|
-
actAs: REMOTE_ADMIN_LENS,
|
|
979
1060
|
});
|
|
980
1061
|
},
|
|
981
1062
|
|
|
982
1063
|
/** Delete the subtree at this path (`rtdb.remove`). */
|
|
983
1064
|
async remove(): Promise<void> {
|
|
984
|
-
await state
|
|
1065
|
+
await relayRtdbOp(state, {
|
|
985
1066
|
method: 'rtdb.remove',
|
|
986
1067
|
path: canonical,
|
|
987
|
-
actAs: REMOTE_ADMIN_LENS,
|
|
988
1068
|
});
|
|
989
1069
|
},
|
|
990
1070
|
|
|
@@ -1003,14 +1083,7 @@ function buildRemoteRef(
|
|
|
1003
1083
|
const write: Promise<void> =
|
|
1004
1084
|
value === undefined
|
|
1005
1085
|
? Promise.resolve()
|
|
1006
|
-
: state.
|
|
1007
|
-
.op({
|
|
1008
|
-
method: 'rtdb.push',
|
|
1009
|
-
path: canonical,
|
|
1010
|
-
key: id,
|
|
1011
|
-
value,
|
|
1012
|
-
actAs: REMOTE_ADMIN_LENS,
|
|
1013
|
-
})
|
|
1086
|
+
: relayRtdbOp(state, { method: 'rtdb.push', path: canonical, key: id, value })
|
|
1014
1087
|
.then(() => undefined);
|
|
1015
1088
|
// Surface completion without forcing the caller to await: `.then`'s
|
|
1016
1089
|
// rejection handler also keeps a fire-and-forget push from becoming
|
|
@@ -1069,8 +1142,9 @@ function buildRemoteRef(
|
|
|
1069
1142
|
typeof cancelCallbackOrContext === 'function'
|
|
1070
1143
|
? (cancelCallbackOrContext as (err: Error) => unknown)
|
|
1071
1144
|
: undefined;
|
|
1072
|
-
const detach =
|
|
1073
|
-
|
|
1145
|
+
const detach = subscribeRtdbValue(
|
|
1146
|
+
state,
|
|
1147
|
+
canonical,
|
|
1074
1148
|
(value) => {
|
|
1075
1149
|
callback(snapFromWire(value as RemoteWireSnapshot));
|
|
1076
1150
|
},
|
package/src/deferred.ts
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Unmodeled `firebase-admin/*` subpaths and their `App`-bound service handles.
|
|
3
3
|
*
|
|
4
4
|
* Under `pyric sandbox`, every `firebase-admin/*` import resolves to the same
|
|
5
|
-
* subpath of `pyric-admin`.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* subpath of `pyric-admin`. Many server codebases initialize unmodeled services
|
|
6
|
+
* unconditionally at module load (`export const appCheck = getAppCheck(app)`)
|
|
7
|
+
* while never invoking methods on that handle on sandboxed request paths.
|
|
8
|
+
*
|
|
9
|
+
* Mirroring upstream's container contract keeps module initialization faithful
|
|
10
|
+
* without ever reaching production Firebase:
|
|
11
|
+
* - `get<Service>(app?)` resolves the active `PyricAdminApp` (throwing
|
|
12
|
+
* `FirebaseAppError` `app/no-app` before `initializeApp()`, or
|
|
13
|
+
* `app/app-deleted` after `deleteApp()`), caches one handle per app, and
|
|
14
|
+
* exposes `.app`.
|
|
15
|
+
* - Every method on the returned handle is wired via `defineDeferredMembers` so
|
|
16
|
+
* calling it throws synchronously or returns a rejected `Promise` with
|
|
17
|
+
* `PyricDeferredApiError` naming `<Class>.<method>` and `pyric-admin/<subpath>`.
|
|
18
|
+
* - Helper classes, enums, and direct `new <Service>()` calls throw
|
|
19
|
+
* `PyricDeferredApiError` naming the subpath.
|
|
11
20
|
*/
|
|
12
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
deferredEntry,
|
|
23
|
+
defineDeferredMembers,
|
|
24
|
+
PyricDeferredApiError,
|
|
25
|
+
type DeferredApi,
|
|
26
|
+
type DeferredMembers,
|
|
27
|
+
} from 'pyric/app/internal';
|
|
28
|
+
import { getApp, type PyricAdminApp } from './app/index.js';
|
|
29
|
+
import { assertAdminAppActive } from './app/lifecycle.js';
|
|
30
|
+
|
|
31
|
+
export { PyricDeferredApiError };
|
|
13
32
|
|
|
14
|
-
|
|
33
|
+
/** Private construction token so `new <Service>()` throws while `get<Service>(app)` succeeds. */
|
|
34
|
+
export const DEFERRED_SERVICE_TOKEN: unique symbol = Symbol('pyric.admin.deferredService');
|
|
15
35
|
|
|
16
|
-
/** The message every
|
|
17
|
-
function deferredAdminMessage(subpath: string): string {
|
|
36
|
+
/** The message every unmodeled `firebase-admin` export raises when called or constructed directly. */
|
|
37
|
+
export function deferredAdminMessage(subpath: string): string {
|
|
18
38
|
return (
|
|
19
39
|
`pyric: 'firebase-admin/${subpath}' is not mirrored by the local sandbox, so its calls fail here. ` +
|
|
20
40
|
'Imports resolve so module graphs load. Code that needs this service runs against Firebase outside ' +
|
|
@@ -23,11 +43,54 @@ function deferredAdminMessage(subpath: string): string {
|
|
|
23
43
|
}
|
|
24
44
|
|
|
25
45
|
/**
|
|
26
|
-
* The export bag for one deferred `firebase-admin`
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* @param subpath - The subpath without the `firebase-admin/` prefix.
|
|
46
|
+
* The export bag for non-service exports of one deferred `firebase-admin`
|
|
47
|
+
* subpath (helper classes, error classes, enum-like constants).
|
|
30
48
|
*/
|
|
31
49
|
export function deferredAdminEntry(subpath: string): Record<string, DeferredApi> {
|
|
32
50
|
return deferredEntry(subpath, deferredAdminMessage(subpath));
|
|
33
51
|
}
|
|
52
|
+
|
|
53
|
+
/** Resolve the default or explicit admin `App` and assert it has not been deleted. */
|
|
54
|
+
export function resolveActiveAdminApp(app?: PyricAdminApp): PyricAdminApp {
|
|
55
|
+
const resolved = app ?? getApp();
|
|
56
|
+
assertAdminAppActive(resolved);
|
|
57
|
+
return resolved;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Guard direct `new <Service>()` construction while allowing `wireDeferredService` to mint handles. */
|
|
61
|
+
export function guardDeferredServiceApp(
|
|
62
|
+
subpath: string,
|
|
63
|
+
className: string,
|
|
64
|
+
args: readonly unknown[],
|
|
65
|
+
): PyricAdminApp {
|
|
66
|
+
if (args[0] !== DEFERRED_SERVICE_TOKEN) {
|
|
67
|
+
throw new PyricDeferredApiError(subpath, className, deferredAdminMessage(subpath));
|
|
68
|
+
}
|
|
69
|
+
return args[1] as PyricAdminApp;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Attach deferred member stubs onto `ctor.prototype` and return a per-app
|
|
74
|
+
* cached `get<Service>(app?)` factory matching `firebase-admin`'s
|
|
75
|
+
* `firebaseApp.getOrInitService(...)`.
|
|
76
|
+
*/
|
|
77
|
+
export function wireDeferredService<T extends object>(
|
|
78
|
+
ctor: { prototype: T },
|
|
79
|
+
subpath: string,
|
|
80
|
+
owner: string,
|
|
81
|
+
members: DeferredMembers,
|
|
82
|
+
): (app?: PyricAdminApp) => T {
|
|
83
|
+
defineDeferredMembers(ctor.prototype, `pyric-admin/${subpath}`, owner, members);
|
|
84
|
+
const instances = new WeakMap<PyricAdminApp, T>();
|
|
85
|
+
return function getDeferredService(app?: PyricAdminApp): T {
|
|
86
|
+
const resolved = resolveActiveAdminApp(app);
|
|
87
|
+
const existing = instances.get(resolved);
|
|
88
|
+
if (existing !== undefined) return existing;
|
|
89
|
+
const created = new (ctor as unknown as new (...ctorArgs: unknown[]) => T)(
|
|
90
|
+
DEFERRED_SERVICE_TOKEN,
|
|
91
|
+
resolved,
|
|
92
|
+
);
|
|
93
|
+
instances.set(resolved, created);
|
|
94
|
+
return created;
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/eventarc/index.ts
CHANGED
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `pyric-admin/eventarc`:
|
|
3
|
-
*
|
|
4
|
-
* The sandbox does not model Eventarc. Every value `firebase-admin/eventarc`
|
|
5
|
-
* exports is present so imports link; using one throws `PyricDeferredApiError`.
|
|
6
|
-
* See `../deferred.ts`.
|
|
2
|
+
* `pyric-admin/eventarc`: deferred service handle for `firebase-admin/eventarc`.
|
|
7
3
|
*/
|
|
8
|
-
import {
|
|
4
|
+
import type { PyricAdminApp } from '../app/index.js';
|
|
5
|
+
import {
|
|
6
|
+
deferredAdminEntry,
|
|
7
|
+
guardDeferredServiceApp,
|
|
8
|
+
wireDeferredService,
|
|
9
|
+
} from '../deferred.js';
|
|
9
10
|
|
|
10
11
|
export { PyricDeferredApiError } from '../deferred.js';
|
|
11
12
|
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
export class Eventarc {
|
|
14
|
+
readonly #app: PyricAdminApp;
|
|
15
|
+
|
|
16
|
+
/** @internal */
|
|
17
|
+
constructor(...args: unknown[]) {
|
|
18
|
+
this.#app = guardDeferredServiceApp('eventarc', 'Eventarc', args);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get app(): PyricAdminApp {
|
|
22
|
+
return this.#app;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare channel: (...args: unknown[]) => never;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const getEventarc: (app?: PyricAdminApp) => Eventarc = wireDeferredService(
|
|
29
|
+
Eventarc,
|
|
30
|
+
'eventarc',
|
|
31
|
+
'Eventarc',
|
|
32
|
+
{
|
|
33
|
+
sync: ['channel'],
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export const { Channel } = deferredAdminEntry('eventarc');
|