Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fis): Adding the admin.installations() API for deleting Firebase installation IDs #1187

Merged
merged 10 commits into from
Jun 23, 2021
16 changes: 16 additions & 0 deletions etc/firebase-admin.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export namespace app {
// (undocumented)
firestore(): firestore.Firestore;
// (undocumented)
installations(): installations.Installations;
// @deprecated (undocumented)
instanceId(): instanceId.InstanceId;
// (undocumented)
machineLearning(): machineLearning.MachineLearning;
Expand Down Expand Up @@ -498,13 +500,27 @@ export interface GoogleOAuthAccessToken {
export function initializeApp(options?: AppOptions, name?: string): app.App;

// @public
export function installations(app?: app.App): installations.Installations;

// @public (undocumented)
export namespace installations {
export interface Installations {
// (undocumented)
app: app.App;
deleteInstallation(fid: string): Promise<void>;
}
}

// @public @deprecated
export function instanceId(app?: app.App): instanceId.InstanceId;

// @public (undocumented)
export namespace instanceId {
// @deprecated
export interface InstanceId {
// (undocumented)
app: app.App;
// @deprecated
deleteInstanceId(instanceId: string): Promise<void>;
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/firebase-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { database } from './database/index';
import { DatabaseService } from './database/database-internal';
import { Firestore } from '@google-cloud/firestore';
import { FirestoreService } from './firestore/firestore-internal';
import { Installations } from './installations/installations';
import { InstanceId } from './instance-id/instance-id';
import { ProjectManagement } from './project-management/project-management';
import { SecurityRules } from './security-rules/security-rules';
Expand Down Expand Up @@ -327,9 +328,23 @@ export class FirebaseApp implements app.App {
return service.client;
}

/**
* Returns the `Installations` service instance associated with this app.
*
* @return The `Installations` service instance of this app.
*/
public installations(): Installations {
return this.ensureService_('installations', () => {
const fisService: typeof Installations = require('./installations/installations').Installations;
return new fisService(this);
});
}

/**
* Returns the InstanceId service instance associated with this app.
*
* This API is deprecated. Use the `installations()` API instead.
*
* @return The InstanceId service instance of this app.
*/
public instanceId(): InstanceId {
Expand Down
3 changes: 3 additions & 0 deletions src/firebase-namespace-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { auth } from './auth/index';
import { credential } from './credential/index';
import { database } from './database/index';
import { firestore } from './firestore/index';
import { installations } from './installations/index';
import { instanceId } from './instance-id/index';
import { machineLearning } from './machine-learning/index';
import { messaging } from './messaging/index';
Expand Down Expand Up @@ -225,6 +226,8 @@ export namespace app {
auth(): auth.Auth;
database(url?: string): database.Database;
firestore(): firestore.Firestore;
installations(): installations.Installations;
/** @deprecated */
instanceId(): instanceId.InstanceId;
machineLearning(): machineLearning.MachineLearning;
messaging(): messaging.Messaging;
Expand Down
1 change: 1 addition & 0 deletions src/firebase-namespace.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from './firebase-namespace-api';
export * from './auth/index';
export * from './database/index';
export * from './firestore/index';
export * from './installations/index';
export * from './instance-id/index';
export * from './machine-learning/index';
export * from './messaging/index';
Expand Down
14 changes: 14 additions & 0 deletions src/firebase-namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { getApplicationDefault } from './credential/credential-internal';
import { auth } from './auth/index';
import { database } from './database/index';
import { firestore } from './firestore/index';
import { installations } from './installations/index';
import { instanceId } from './instance-id/index';
import { machineLearning } from './machine-learning/index';
import { messaging } from './messaging/index';
Expand All @@ -41,6 +42,7 @@ import App = app.App;
import Auth = auth.Auth;
import Database = database.Database;
import Firestore = firestore.Firestore;
import Installations = installations.Installations;
import InstanceId = instanceId.InstanceId;
import MachineLearning = machineLearning.MachineLearning;
import Messaging = messaging.Messaging;
Expand Down Expand Up @@ -309,6 +311,18 @@ export class FirebaseNamespace {
return Object.assign(fn, { MachineLearning: machineLearning });
}

/**
* Gets the `Installations` service namespace. The returned namespace can be used to get the
* `Installations` service for the default app or an explicitly specified app.
*/
get installations(): FirebaseServiceNamespace<Installations> {
const fn: FirebaseServiceNamespace<Installations> = (app?: App) => {
return this.ensureApp(app).installations();
};
const installations = require('./installations/installations').Installations;
return Object.assign(fn, { Installations: installations });
}

/**
* Gets the `InstanceId` service namespace. The returned namespace can be used to get the
* `Instance` service for the default app or an explicitly specified app.
Expand Down
85 changes: 85 additions & 0 deletions src/installations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*!
* Copyright 2021 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { app } from '../firebase-namespace-api';

/**
* Gets the {@link installations.Installations `Installations`} service for the
* default app or a given app.
*
* `admin.installations()` can be called with no arguments to access the default
* app's {@link installations.Installations `Installations`} service or as
* `admin.installations(app)` to access the
* {@link installations.Installations `Installations`} service associated with a
* specific app.
*
* @example
* ```javascript
* // Get the Installations service for the default app
* var defaultInstallations = admin.installations();
* ```
*
* @example
* ```javascript
* // Get the Installations service for a given app
* var otherInstallations = admin.installations(otherApp);
*```
*
* @param app Optional app whose `Installations` service to
* return. If not provided, the default `Installations` service will be
Copy link
Contributor

@egilmorez egilmorez Apr 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Optional app whose Installations service to

  • return." makes sense but isn't 100% grammatical.

"Optional app for which to return the Installations service" is valid but not great.

"Optional app whose Installations service should be

  • returned." sounds better, but "should" seems weak.

I'll poll the tech writers and see who has a solution :)

* returned.
hiranya911 marked this conversation as resolved.
Show resolved Hide resolved
*
* @return The default `Installations` service if
* no app is provided or the `Installations` service associated with the
* provided app.
*/
export declare function installations(app?: app.App): installations.Installations;

/* eslint-disable @typescript-eslint/no-namespace */
export namespace installations {
/**
* Gets the {@link Installations `Installations`} service for the
* current app.
*
* @example
* ```javascript
* var installations = app.installations();
* // The above is shorthand for:
* // var installations = admin.installations(app);
* ```
*
* @return The `Installations` service for the
* current app.
*/
export interface Installations {
app: app.App;

/**
* Deletes the specified installation ID and the associated data from Firebase.
*
* Note that Google Analytics for Firebase uses its own form of Instance ID to
* keep track of analytics data. Therefore deleting a Firebase installation ID does
* not delete Analytics data. See
* [Delete an Instance ID](/docs/projects/manage-installations#delete-installation)
hiranya911 marked this conversation as resolved.
Show resolved Hide resolved
* for more information.
*
* @param fid The Firebase installation ID to be deleted.
*
* @return A promise fulfilled when the installation ID is deleted.
*/
deleteInstallation(fid: string): Promise<void>;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*!
* @license
* Copyright 2017 Google Inc.
* Copyright 2021 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,7 +16,7 @@
*/

import { FirebaseApp } from '../firebase-app';
import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../utils/error';
import { FirebaseInstallationsError, InstallationsClientErrorCode } from '../utils/error';
import {
ApiSettings, AuthorizedHttpClient, HttpRequestConfig, HttpError,
} from '../utils/api-request';
Expand All @@ -33,50 +33,50 @@ const FIREBASE_IID_TIMEOUT = 10000;

/** HTTP error codes raised by the backend server. */
const ERROR_CODES: {[key: number]: string} = {
400: 'Malformed instance ID argument.',
400: 'Malformed installation ID argument.',
401: 'Request not authorized.',
403: 'Project does not match instance ID or the client does not have sufficient privileges.',
404: 'Failed to find the instance ID.',
403: 'Project does not match installation ID or the client does not have sufficient privileges.',
404: 'Failed to find the installation ID.',
409: 'Already deleted.',
429: 'Request throttled out by the backend server.',
500: 'Internal server error.',
503: 'Backend servers are over capacity. Try again later.',
};

/**
* Class that provides mechanism to send requests to the Firebase Instance ID backend endpoints.
* Class that provides mechanism to send requests to the FIS backend endpoints.
*/
export class FirebaseInstanceIdRequestHandler {
export class FirebaseInstallationsRequestHandler {

private readonly host: string = FIREBASE_IID_HOST;
private readonly timeout: number = FIREBASE_IID_TIMEOUT;
private readonly httpClient: AuthorizedHttpClient;
private path: string;

/**
* @param {FirebaseApp} app The app used to fetch access tokens to sign API requests.
* @param app The app used to fetch access tokens to sign API requests.
*
* @constructor
*/
constructor(private readonly app: FirebaseApp) {
this.httpClient = new AuthorizedHttpClient(app);
}

public deleteInstanceId(instanceId: string): Promise<void> {
if (!validator.isNonEmptyString(instanceId)) {
return Promise.reject(new FirebaseInstanceIdError(
InstanceIdClientErrorCode.INVALID_INSTANCE_ID,
'Instance ID must be a non-empty string.',
public deleteInstallation(fid: string): Promise<void> {
if (!validator.isNonEmptyString(fid)) {
return Promise.reject(new FirebaseInstallationsError(
InstallationsClientErrorCode.INVALID_INSTALLATION_ID,
'Installation ID must be a non-empty string.',
));
}
return this.invokeRequestHandler(new ApiSettings(instanceId, 'DELETE'));
return this.invokeRequestHandler(new ApiSettings(fid, 'DELETE'));
}

/**
* Invokes the request handler based on the API settings object passed.
*
* @param {ApiSettings} apiSettings The API endpoint settings to apply to request and response.
* @return {Promise<void>} A promise that resolves when the request is complete.
* @param apiSettings The API endpoint settings to apply to request and response.
* @return A promise that resolves when the request is complete.
*/
private invokeRequestHandler(apiSettings: ApiSettings): Promise<void> {
return this.getPathPrefix()
Expand All @@ -98,8 +98,8 @@ export class FirebaseInstanceIdRequestHandler {
response.data.error : response.text;
const template: string = ERROR_CODES[response.status];
const message: string = template ?
`Instance ID "${apiSettings.getEndpoint()}": ${template}` : errorMessage;
throw new FirebaseInstanceIdError(InstanceIdClientErrorCode.API_ERROR, message);
`Installation ID "${apiSettings.getEndpoint()}": ${template}` : errorMessage;
throw new FirebaseInstallationsError(InstallationsClientErrorCode.API_ERROR, message);
}
// In case of timeouts and other network errors, the HttpClient returns a
// FirebaseError wrapped in the response. Simply throw it here.
Expand All @@ -116,9 +116,9 @@ export class FirebaseInstanceIdRequestHandler {
.then((projectId) => {
if (!validator.isNonEmptyString(projectId)) {
// Assert for an explicit projct ID (either via AppOptions or the cert itself).
throw new FirebaseInstanceIdError(
InstanceIdClientErrorCode.INVALID_PROJECT_ID,
'Failed to determine project ID for InstanceId. Initialize the '
throw new FirebaseInstallationsError(
InstallationsClientErrorCode.INVALID_PROJECT_ID,
'Failed to determine project ID for Installations. Initialize the '
+ 'SDK with service account credentials or set project ID as an app option. '
+ 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.',
);
Expand Down
68 changes: 68 additions & 0 deletions src/installations/installations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*!
* Copyright 2021 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { FirebaseApp } from '../firebase-app';
import { FirebaseInstallationsError, InstallationsClientErrorCode } from '../utils/error';
import { FirebaseInstallationsRequestHandler } from './installations-request-handler';
import { installations } from './index';
import * as validator from '../utils/validator';

import InstallationsInterface = installations.Installations;

/**
* The `Installations` service for the current app.
*/
export class Installations implements InstallationsInterface {

private app_: FirebaseApp;
private requestHandler: FirebaseInstallationsRequestHandler;

/**
* @param app The app for this Installations service.
* @constructor
*/
constructor(app: FirebaseApp) {
if (!validator.isNonNullObject(app) || !('options' in app)) {
throw new FirebaseInstallationsError(
InstallationsClientErrorCode.INVALID_ARGUMENT,
'First argument passed to admin.installations() must be a valid Firebase app instance.',
);
}

this.app_ = app;
this.requestHandler = new FirebaseInstallationsRequestHandler(app);
}

/**
* Deletes the specified installation ID and the associated data from Firebase.
*
* @param fid The Firebase installation ID to be deleted.
*
* @return A promise fulfilled when the installation ID is deleted.
*/
public deleteInstallation(fid: string): Promise<void> {
return this.requestHandler.deleteInstallation(fid);
}

/**
* Returns the app associated with this Installations instance.
*
* @return The app associated with this Installations instance.
*/
get app(): FirebaseApp {
return this.app_;
}
}