diff --git a/src/firebase-app.ts b/src/firebase-app.ts index b58f6ac019..c363afa24f 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -22,6 +22,7 @@ import {FirebaseNamespaceInternals} from './firebase-namespace'; import {AppErrorCodes, FirebaseAppError} from './utils/error'; import {Auth} from './auth/auth'; +import {MachineLearning} from './machine-learning/machine-learning'; import {Messaging} from './messaging/messaging'; import {Storage} from './storage/storage'; import {Database} from '@firebase/database'; @@ -29,6 +30,7 @@ import {DatabaseService} from './database/database'; import {Firestore} from '@google-cloud/firestore'; import {FirestoreService} from './firestore/firestore'; import {InstanceId} from './instance-id/instance-id'; + import {ProjectManagement} from './project-management/project-management'; import {SecurityRules} from './security-rules/security-rules'; @@ -354,6 +356,19 @@ export class FirebaseApp { }); } + /** + * Returns the MachineLearning service instance associated with this app. + * + * @return {MachineLearning} The Machine Learning service instance of this app + */ + public machineLearning(): MachineLearning { + return this.ensureService_('machine-learning', () => { + const machineLearningService: typeof MachineLearning = + require('./machine-learning/machine-learning').MachineLearning; + return new machineLearningService(this); + }); + } + /** * Returns the ProjectManagement service instance associated with this app. * diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index 738fc0d4d7..dea6756d0b 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -28,6 +28,7 @@ import { } from './auth/credential'; import {Auth} from './auth/auth'; +import {MachineLearning} from './machine-learning/machine-learning'; import {Messaging} from './messaging/messaging'; import {Storage} from './storage/storage'; import {Database} from '@firebase/database'; @@ -396,6 +397,21 @@ export class FirebaseNamespace { return fn; } + /** + * Gets the `MachineLearning` service namespace. The returned namespace can be + * used to get the `MachineLearning` service for the default app or an + * explicityly specified app. + */ + get machineLearning(): FirebaseServiceNamespace { + const fn: FirebaseServiceNamespace = + (app?: FirebaseApp) => { + return this.ensureApp(app).machineLearning(); + }; + const machineLearning = + require('./machine-learning/machine-learning').MachineLearning; + return Object.assign(fn, {MachineLearning: machineLearning}); + } + /** * 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. diff --git a/src/index.d.ts b/src/index.d.ts index 5177647da2..79d498e219 100755 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -271,6 +271,38 @@ declare namespace admin { */ function messaging(app?: admin.app.App): admin.messaging.Messaging; + /** + * Gets the {@link admin.ml.MachineLearning `MachineLearning`} service for the + * default app or a given app. + * + * `admin.machineLearning()` can be called with no arguments to access the + * default app's {@link admin.machineLearing.MachineLearning + * `MachineLearning`} service or as `admin.machineLearning(app)` to access + * the {@link admin.machineLearning.MachineLearning `MachineLearning`} + * service associated with a specific app. + * + * @example + * ```javascript + * // Get the MachineLearning service for the default app + * var defaultMachineLearning = admin.machineLearning(); + * ``` + * + * @example + * ```javascript + * // Get the MachineLearning service for a given app + * var otherMachineLearning = admin.machineLearning(otherApp); + * ``` + * + * @param app Optional app whose `MachineLearning` service to + * return. If not provided, the default `MachineLearning` service + * will be returned. + * + * @return The default `MachineLearning` service if no app is provided or the + * `MachineLearning` service associated with the provided app. + */ + function machineLearning(app?: admin.app.App): + admin.machineLearning.MachineLearning; + /** * Gets the {@link admin.storage.Storage `Storage`} service for the * default app or a given app. @@ -454,6 +486,7 @@ declare namespace admin.app { firestore(): admin.firestore.Firestore; instanceId(): admin.instanceId.InstanceId; messaging(): admin.messaging.Messaging; + machineLearning(): admin.machineLearning.MachineLearning; projectManagement(): admin.projectManagement.ProjectManagement; securityRules(): admin.securityRules.SecurityRules; storage(): admin.storage.Storage; @@ -1099,14 +1132,14 @@ declare namespace admin.auth { /** * Interface representing a tenant configuration. - * + * * Multi-tenancy support requires Google Cloud's Identity Platform * (GCIP). To learn more about GCIP, including pricing and features, * see the [GCIP documentation](https://cloud.google.com/identity-platform) - * + * * Before multi-tenancy can be used on a Google Cloud Identity Platform project, * tenants must be allowed on that project via the Cloud Console UI. - * + * * A tenant configuration provides information such as the display name, tenant * identifier and email authentication configuration. * For OIDC/SAML provider configuration management, `TenantAwareAuth` instances should @@ -1189,7 +1222,7 @@ declare namespace admin.auth { /** * Interface representing the object returned from a * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listTenants `listTenants()`} - * operation. + * operation. * Contains the list of tenants for the current batch and the next page token if available. */ interface ListTenantsResult { @@ -1995,7 +2028,7 @@ declare namespace admin.auth { * */ interface TenantManager { - /** + /** * @param tenantId The tenant ID whose `TenantAwareAuth` instance is to be returned. * * @return The `TenantAwareAuth` instance corresponding to this tenant identifier. @@ -2035,7 +2068,7 @@ declare namespace admin.auth { */ deleteTenant(tenantId: string): Promise; - /** + /** * Creates a new tenant. * When creating new tenants, tenants that use separate billing and quota will require their * own project and must be defined as `full_service`. @@ -4004,15 +4037,15 @@ declare namespace admin.messaging { channelId?: string; /** - * Sets the "ticker" text, which is sent to accessibility services. Prior to - * API level 21 (Lollipop), sets the text that is displayed in the status bar + * Sets the "ticker" text, which is sent to accessibility services. Prior to + * API level 21 (Lollipop), sets the text that is displayed in the status bar * when the notification first arrives. */ ticker?: string; /** - * When set to `false` or unset, the notification is automatically dismissed when - * the user clicks it in the panel. When set to `true`, the notification persists + * When set to `false` or unset, the notification is automatically dismissed when + * the user clicks it in the panel. When set to `true`, the notification persists * even when the user clicks it. */ sticky?: boolean; @@ -4025,73 +4058,73 @@ declare namespace admin.messaging { eventTimestamp?: Date; /** - * Sets whether or not this notification is relevant only to the current device. - * Some notifications can be bridged to other devices for remote display, such as - * a Wear OS watch. This hint can be set to recommend this notification not be bridged. + * Sets whether or not this notification is relevant only to the current device. + * Some notifications can be bridged to other devices for remote display, such as + * a Wear OS watch. This hint can be set to recommend this notification not be bridged. * See [Wear OS guides](https://developer.android.com/training/wearables/notifications/bridger#existing-method-of-preventing-bridging) */ localOnly?: boolean; /** - * Sets the relative priority for this notification. Low-priority notifications - * may be hidden from the user in certain situations. Note this priority differs - * from `AndroidMessagePriority`. This priority is processed by the client after - * the message has been delivered. Whereas `AndroidMessagePriority` is an FCM concept + * Sets the relative priority for this notification. Low-priority notifications + * may be hidden from the user in certain situations. Note this priority differs + * from `AndroidMessagePriority`. This priority is processed by the client after + * the message has been delivered. Whereas `AndroidMessagePriority` is an FCM concept * that controls when the message is delivered. */ priority?: ('min' | 'low' | 'default' | 'high' | 'max'); /** - * Sets the vibration pattern to use. Pass in an array of milliseconds to - * turn the vibrator on or off. The first value indicates the duration to wait before - * turning the vibrator on. The next value indicates the duration to keep the - * vibrator on. Subsequent values alternate between duration to turn the vibrator - * off and to turn the vibrator on. If `vibrate_timings` is set and `default_vibrate_timings` + * Sets the vibration pattern to use. Pass in an array of milliseconds to + * turn the vibrator on or off. The first value indicates the duration to wait before + * turning the vibrator on. The next value indicates the duration to keep the + * vibrator on. Subsequent values alternate between duration to turn the vibrator + * off and to turn the vibrator on. If `vibrate_timings` is set and `default_vibrate_timings` * is set to `true`, the default value is used instead of the user-specified `vibrate_timings`. */ vibrateTimingsMillis?: number[]; /** - * If set to `true`, use the Android framework's default vibrate pattern for the - * notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). - * If `default_vibrate_timings` is set to `true` and `vibrate_timings` is also set, + * If set to `true`, use the Android framework's default vibrate pattern for the + * notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + * If `default_vibrate_timings` is set to `true` and `vibrate_timings` is also set, * the default value is used instead of the user-specified `vibrate_timings`. */ defaultVibrateTimings?: boolean; /** - * If set to `true`, use the Android framework's default sound for the notification. + * If set to `true`, use the Android framework's default sound for the notification. * Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). */ defaultSound?: boolean; /** - * Settings to control the notification's LED blinking rate and color if LED is + * Settings to control the notification's LED blinking rate and color if LED is * available on the device. The total blinking time is controlled by the OS. */ lightSettings?: LightSettings; /** - * If set to `true`, use the Android framework's default LED light settings - * for the notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). - * If `default_light_settings` is set to `true` and `light_settings` is also set, + * If set to `true`, use the Android framework's default LED light settings + * for the notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + * If `default_light_settings` is set to `true` and `light_settings` is also set, * the user-specified `light_settings` is used instead of the default value. */ defaultLightSettings?: boolean; /** - * Sets the visibility of the notification. Must be either `private`, `public`, + * Sets the visibility of the notification. Must be either `private`, `public`, * or `secret`. If unspecified, defaults to `private`. */ visibility?: ('private' | 'public' | 'secret'); /** - * Sets the number of items this notification represents. May be displayed as a - * badge count for Launchers that support badging. See [`NotificationBadge`(https://developer.android.com/training/notify-user/badges). + * Sets the number of items this notification represents. May be displayed as a + * badge count for Launchers that support badging. See [`NotificationBadge`(https://developer.android.com/training/notify-user/badges). * For example, this might be useful if you're using just one notification to * represent multiple new messages but you want the count here to represent - * the number of total new messages. If zero or unspecified, systems - * that support badging use the default, which is to increment a number + * the number of total new messages. If zero or unspecified, systems + * that support badging use the default, which is to increment a number * displayed on the long-press menu each time a new notification arrives. */ notificationCount?: number; @@ -4113,7 +4146,7 @@ declare namespace admin.messaging { lightOnDurationMillis: number; /** - * Required. Along with `light_on_duration`, defines the blink rate of LED flashes. + * Required. Along with `light_on_duration`, defines the blink rate of LED flashes. */ lightOffDurationMillis: number; } @@ -5170,6 +5203,138 @@ declare namespace admin.messaging { } } +declare namespace admin.machineLearning { + /** + * Interface representing options for listing Models. + */ + interface ListModelOptions { + listFilter?: string; + pageSize?: number; + pageToken?: string; + } + + /** Response object for a listModels operation. */ + interface ListModelsResult { + readonly models: Model[]; + readonly pageToken?: string; + } + + /** + * A TFLite Model output object + */ + interface TFLiteModel { + readonly sizeBytes: number; + + readonly gcsTfliteUri?: string; + } + + /** + * A Firebase ML Model input object + */ + interface ModelOptions { + displayName?: string; + tags?: string[]; + + tfLiteModel?: {gcsTFLiteUri: string;}; + + toJSON(forUpload?: boolean): object; + } + + /** + * A Firebase ML Model output object + */ + interface Model { + readonly modelId: string; + readonly displayName: string; + readonly tags?: string[]; + readonly createTime: number; + readonly updateTime: number; + readonly validationError?: string; + readonly published: boolean; + readonly etag: string; + readonly modelHash: string; + readonly locked: boolean; + waitForUnlocked(maxTimeSeconds?: number): Promise; + + readonly tfLiteModel?: TFLiteModel; + } + + /** + * The Firebase `MachineLearning` service interface. + * + * Do not call this constructor directly. Instead, use + * [`admin.machineLearning()`](admin.machineLearning#machineLearning). + */ + interface MachineLearning { + app: admin.app.App; + + /** + * Creates a model in Firebase ML. + * + * @param {ModelOptions} model The model to create. + * + * @return {Promise} A Promise fulfilled with the created model. + */ + createModel(model: ModelOptions): Promise; + + /** + * Updates a model in Firebase ML. + * + * @param {string} modelId The id of the model to update. + * @param {ModelOptions} model The model fields to update. + * + * @return {Promise} A Promise fulfilled with the updated model. + */ + updateModel(modelId: string, model: ModelOptions): Promise; + + /** + * Publishes a model in Firebase ML. + * + * @param {string} modelId The id of the model to publish. + * + * @return {Promise} A Promise fulfilled with the published model. + */ + publishModel(modelId: string): Promise; + + /** + * Unpublishes a model in Firebase ML. + * + * @param {string} modelId The id of the model to unpublish. + * + * @return {Promise} A Promise fulfilled with the unpublished model. + */ + unpublishModel(modelId: string): Promise; + + /** + * Gets a model from Firebase ML. + * + * @param {string} modelId The id of the model to get. + * + * @return {Promise} A Promise fulfilled with the unpublished model. + */ + getModel(modelId: string): Promise; + + /** + * Lists models from Firebase ML. + * + * @param {ListModelsOptions} options The listing options. + * + * @return {Promise<{models: Model[], pageToken?: string}>} A promise that + * resolves with the current (filtered) list of models and the next page + * token. For the last page, an empty list of models and no page token + * are returned. + */ + listModels(options: ListModelOptions): Promise; + + /** + * Deletes a model from Firebase ML. + * + * @param {string} modelId The id of the model to delete. + */ + deleteModel(modelId: string): Promise; + } +} + declare namespace admin.storage { /** diff --git a/src/machine-learning/machine-learning.ts b/src/machine-learning/machine-learning.ts new file mode 100644 index 0000000000..04b648d07b --- /dev/null +++ b/src/machine-learning/machine-learning.ts @@ -0,0 +1,219 @@ +/*! + * Copyright 2020 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 {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; +import {FirebaseError} from '../utils/error'; + +import * as validator from '../utils/validator'; + +// const ML_HOST = 'mlkit.googleapis.com'; + +/** + * Internals of an ML instance. + */ +class MachineLearningInternals implements FirebaseServiceInternalsInterface { + /** + * Deletes the service and its associated resources. + * + * @return {Promise} An empty Promise that will be resolved when the + * service is deleted. + */ + public delete(): Promise { + // There are no resources to clean up. + return Promise.resolve(); + } +} + +/** Interface representing listModels options. */ +export interface ListModelsOptions { + listFilter?: string; + pageSize?: number; + pageToken?: string; +} + +/** Response object for a listModels operation. */ +export interface ListModelsResult { + models: Model[]; + pageToken?: string; +} + +/** + * The Firebase Machine Learning class + */ +export class MachineLearning implements FirebaseServiceInterface { + public readonly INTERNAL = new MachineLearningInternals(); + + private readonly appInternal: FirebaseApp; + + /** + * @param {FirebaseApp} app The app for this ML service. + * @constructor + */ + constructor(app: FirebaseApp) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseError({ + code: 'machine-learning/invalid-argument', + message: 'First argument passed to admin.MachineLearning() must be a ' + + 'valid Firebase app instance.', + }); + } + + this.appInternal = app; + } + + /** + * Returns the app associated with this ML instance. + * + * @return {FirebaseApp} The app associated with this ML instance. + */ + public get app(): FirebaseApp { + return this.appInternal; + } + + /** + * Creates a model in Firebase ML. + * + * @param {ModelOptions} model The model to create. + * + * @return {Promise} A Promise fulfilled with the created model. + */ + public createModel(model: ModelOptions): Promise { + throw new Error('NotImplemented'); + } + + /** + * Updates a model in Firebase ML. + * + * @param {string} modelId The id of the model to update. + * @param {ModelOptions} model The model fields to update. + * + * @return {Promise} A Promise fulfilled with the updated model. + */ + public updateModel(modelId: string, model: ModelOptions): Promise { + throw new Error('NotImplemented'); + } + + /** + * Publishes a model in Firebase ML. + * + * @param {string} modelId The id of the model to publish. + * + * @return {Promise} A Promise fulfilled with the published model. + */ + public publishModel(modelId: string): Promise { + throw new Error('NotImplemented'); + } + + /** + * Unpublishes a model in Firebase ML. + * + * @param {string} modelId The id of the model to unpublish. + * + * @return {Promise} A Promise fulfilled with the unpublished model. + */ + public unpublishModel(modelId: string): Promise { + throw new Error('NotImplemented'); + } + + /** + * Gets a model from Firebase ML. + * + * @param {string} modelId The id of the model to get. + * + * @return {Promise} A Promise fulfilled with the unpublished model. + */ + public getModel(modelId: string): Promise { + throw new Error('NotImplemented'); + } + + /** + * Lists models from Firebase ML. + * + * @param {ListModelsOptions} options The listing options. + * + * @return {Promise<{models: Model[], pageToken?: string}>} A promise that + * resolves with the current (filtered) list of models and the next page + * token. For the last page, an empty list of models and no page token are + * returned. + */ + public listModels(options: ListModelsOptions): Promise { + throw new Error('NotImplemented'); + } + + /** + * Deletes a model from Firebase ML. + * + * @param {string} modelId The id of the model to delete. + */ + public deleteModel(modelId: string): Promise { + throw new Error('NotImplemented'); + } +} + +/** + * A Firebase ML Model output object + */ +export class Model { + public readonly modelId: string; + public readonly displayName: string; + public readonly tags?: string[]; + public readonly createTime: number; + public readonly updateTime: number; + public readonly validationError?: string; + public readonly published: boolean; + public readonly etag: string; + public readonly modelHash: string; + + public readonly tfLiteModel?: TFLiteModel; + + public get locked(): boolean { + // Backend does not currently return locked models. + // This will likely change in future. + return false; + } + + public waitForUnlocked(maxTimeSeconds?: number): Promise { + // Backend does not currently return locked models. + // This will likely change in future. + return Promise.resolve(); + } +} + +/** + * A TFLite Model output object + */ +export interface TFLiteModel { + readonly sizeBytes: number; + + readonly gcsTfliteUri: string; +} + + +/** + * A Firebase ML Model input object + */ +export class ModelOptions { + public displayName?: string; + public tags?: string[]; + + public tfLiteModel?: { gcsTFLiteUri: string; }; + + protected toJSON(forUpload?: boolean): object { + throw new Error('NotImplemented'); + } +} diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index 5d6fc3063c..42fd427887 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -32,6 +32,7 @@ import {FirebaseNamespace, FirebaseNamespaceInternals, FIREBASE_CONFIG_VAR} from import {Auth} from '../../src/auth/auth'; import {Messaging} from '../../src/messaging/messaging'; +import {MachineLearning} from '../../src/machine-learning/machine-learning'; import {Storage} from '../../src/storage/storage'; import {Firestore} from '@google-cloud/firestore'; import {Database} from '@firebase/database'; @@ -395,6 +396,32 @@ describe('FirebaseApp', () => { }); }); + describe('machineLearning()', () => { + it('should throw if the app has already been deleted', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + + return app.delete().then(() => { + expect(() => { + return app.machineLearning(); + }).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`); + }); + }); + + it('should return the machineLearning client', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + + const machineLearning: MachineLearning = app.machineLearning(); + expect(machineLearning).to.not.be.null; + }); + + it('should return a cached version of MachineLearning on subsequent calls', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + const service1: MachineLearning = app.machineLearning(); + const service2: MachineLearning = app.machineLearning(); + expect(service1).to.equal(service2); + }); + }); + describe('database()', () => { afterEach(() => { try { diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index f52b50bfc9..017701c2a7 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -37,6 +37,7 @@ import { ServerValue, } from '@firebase/database'; import {Messaging} from '../../src/messaging/messaging'; +import {MachineLearning} from '../../src/machine-learning/machine-learning'; import {Storage} from '../../src/storage/storage'; import { Firestore, @@ -473,6 +474,38 @@ describe('FirebaseNamespace', () => { }); }); + describe('#machine-learning()', () => { + it('should throw when called before initializating an app', () => { + expect(() => { + firebaseNamespace.machineLearning(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.machineLearning(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should return a valid namespace when the default app is initialized', () => { + const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const ml: MachineLearning = firebaseNamespace.machineLearning(); + expect(ml.app).to.be.deep.equal(app); + }); + + it('should return a valid namespace when the named app is initialized', () => { + const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const ml: MachineLearning = firebaseNamespace.machineLearning(app); + expect(ml.app).to.be.deep.equal(app); + }); + + it('should return a reference to Machine Learning type', () => { + expect(firebaseNamespace.machineLearning.MachineLearning) + .to.be.deep.equal(MachineLearning); + }); + }); + describe('#storage()', () => { it('should throw when called before initializing an app', () => { expect(() => {