diff --git a/.github/scripts/compare-types/packages/storage/config.ts b/.github/scripts/compare-types/packages/storage/config.ts new file mode 100644 index 0000000000..f880e0ca63 --- /dev/null +++ b/.github/scripts/compare-types/packages/storage/config.ts @@ -0,0 +1,210 @@ +/** + * Known differences between the firebase-js-sdk @firebase/storage public + * API and the @react-native-firebase/storage modular API. + * + * Each entry must have a `name` (the export name) and a `reason` explaining + * why the difference exists. Any difference NOT listed here will cause CI to + * fail so that new drift is caught and deliberately acknowledged. + * + * Sections: + * nameMapping — exports that exist in both packages but under different names + * missingInRN — firebase-js-sdk exports absent from RN Firebase + * extraInRN — RN Firebase exports not present in the firebase-js-sdk + * differentShape — exports present in both but with differing signatures/members + */ + +import type { PackageConfig } from '../../src/types'; + +const config: PackageConfig = { + // --------------------------------------------------------------------------- + // Name mapping + // --------------------------------------------------------------------------- + nameMapping: {}, + + // --------------------------------------------------------------------------- + // Missing in RN Firebase + // --------------------------------------------------------------------------- + missingInRN: [ + { + name: 'StorageError', + reason: + 'RN Firebase surfaces Storage failures as `NativeFirebaseError` instances from ' + + '`@react-native-firebase/app` rather than exporting the firebase-js-sdk web ' + + '`StorageError` subclass.', + }, + { + name: 'StorageErrorCode', + reason: + 'RN Firebase does not export the firebase-js-sdk `StorageErrorCode` enum. ' + + 'Errors instead expose native bridge `code` strings via `NativeFirebaseError`.', + }, + ], + + // --------------------------------------------------------------------------- + // Extra in RN Firebase + // --------------------------------------------------------------------------- + extraInRN: [ + { + name: 'setMaxOperationRetryTime', + reason: + 'RN Firebase-specific function for setting the maximum retry time for ' + + 'non-upload/download operations on Android and iOS. The firebase-js-sdk ' + + 'exposes this as a writable property on the `FirebaseStorage` instance.', + }, + { + name: 'setMaxUploadRetryTime', + reason: + 'RN Firebase-specific function for setting the maximum upload retry time ' + + 'on Android and iOS. The firebase-js-sdk exposes this as a writable property ' + + 'on the `FirebaseStorage` instance.', + }, + { + name: 'setMaxDownloadRetryTime', + reason: + 'RN Firebase-specific function for setting the maximum download retry time ' + + 'on Android and iOS. No direct equivalent exists in the firebase-js-sdk.', + }, + { + name: 'putFile', + reason: + 'RN Firebase-specific function that uploads a file from a local device path. ' + + 'No equivalent exists in the firebase-js-sdk web API.', + }, + { + name: 'writeToFile', + reason: + 'RN Firebase-specific function that downloads a file to a local device path. ' + + 'No equivalent exists in the firebase-js-sdk web API.', + }, + { + name: 'TaskSnapshot', + reason: + 'RN Firebase-specific interface extending `UploadTaskSnapshot` with an optional ' + + '`error` field for native task error state. No equivalent in the firebase-js-sdk.', + }, + { + name: 'TaskResult', + reason: + 'RN Firebase type alias for `UploadResult`, kept for API consistency with the ' + + 'native task system. No direct equivalent in the firebase-js-sdk.', + }, + { + name: 'TaskSnapshotObserver', + reason: + 'RN Firebase type alias for `StorageObserver`. ' + + 'No direct equivalent in the firebase-js-sdk.', + }, + { + name: 'Task', + reason: + 'RN Firebase type alias for `UploadTask`, used as the return type of ' + + '`uploadBytesResumable`, `uploadString`, `putFile`, and `writeToFile`. ' + + 'Kept for consistency with the native task system.', + }, + { + name: 'Subscribe', + reason: + 'RN Firebase-specific generic type for task event subscription functions. ' + + 'No direct equivalent in the firebase-js-sdk public API.', + }, + { + name: 'NativeFirebaseError', + reason: + 'Re-export of the RN Firebase native bridge error type used in place of ' + + '`StorageError` / `FirebaseError`. No equivalent in the firebase-js-sdk.', + }, + ], + + // --------------------------------------------------------------------------- + // Different shape + // --------------------------------------------------------------------------- + differentShape: [ + { + name: 'FirebaseStorage', + reason: + 'RN Firebase adds a `maxDownloadRetryTime` property (android & iOS only) ' + + 'not present in the firebase-js-sdk `FirebaseStorage`. Additionally, ' + + '`maxUploadRetryTime` and `maxOperationRetryTime` are declared `readonly` ' + + 'in RN Firebase (they are set via dedicated modular functions) whereas the ' + + 'firebase-js-sdk declares them as mutable properties.', + }, + { + name: 'StorageObserver', + reason: + 'The `error` callback parameter uses `NativeFirebaseError` instead of ' + + '`StorageError`. Both represent Firebase Storage errors but the RN type ' + + 'extends the native bridge error structure.', + }, + { + name: 'UploadTask', + reason: + 'RN Firebase returns `Promise` from `cancel()`, `pause()`, and ' + + '`resume()` to communicate asynchronously with the native iOS/Android modules, ' + + 'whereas the firebase-js-sdk returns a synchronous `boolean`. Error callback ' + + 'types also use `NativeFirebaseError` instead of `StorageError`.', + }, + { + name: 'EmulatorMockTokenOptions', + reason: + 'The firebase-js-sdk `EmulatorMockTokenOptions` (from `@firebase/util`) is a ' + + 'complex type `({ user_id: string } | { sub: string }) & Partial` ' + + 'representing a full mock JWT payload. RN Firebase defines a simplified version ' + + '`{ mockUserToken?: string | null }` that only accepts a pre-encoded token string, ' + + 'since the native bridge does not need to construct JWT payloads directly.', + }, + { + name: 'connectStorageEmulator', + reason: + 'The optional `options` parameter type differs: the firebase-js-sdk accepts ' + + '`{ mockUserToken?: EmulatorMockTokenOptions | string }` (an inline object ' + + 'literal) while RN Firebase accepts its own `EmulatorMockTokenOptions` interface.', + }, + { + name: 'getStream', + reason: + 'Returns `NodeJS.ReadableStream` in RN Firebase instead of `ReadableStream` ' + + '(the Web Streams API type). The Node.js stream type is used because the ' + + 'React Native environment does not have the Web Streams API.', + }, + { + name: 'uploadBytes', + reason: + 'Returns `Promise` in RN Firebase instead of `Promise`. ' + + '`TaskResult` is a type alias for `UploadResult`, so the runtime shape is identical; ' + + 'the different name is for consistency with the native task system.', + }, + { + name: 'uploadBytesResumable', + reason: + 'Returns `Task` in RN Firebase instead of `UploadTask`. `Task` is a type alias ' + + 'for `UploadTask`, so the runtime shape is identical; the different name is for ' + + 'consistency with the native task system.', + }, + { + name: 'uploadString', + reason: + 'Returns `Task` in RN Firebase instead of `Promise` — the upload ' + + 'is resumable via the native task system. The `format` parameter is typed as an ' + + "explicit string union `'raw' | 'base64' | 'base64url' | 'data_url'` instead of " + + 'the `StringFormat` type alias, which is semantically identical.', + }, + { + name: 'TaskEvent', + reason: + "The firebase-js-sdk declares `TaskEvent` as a string literal type `'state_changed'`. " + + 'RN Firebase declares it as a const object `{ STATE_CHANGED: "state_changed" }` and ' + + 'derives the type via `(typeof TaskEvent)[keyof typeof TaskEvent]`. Both resolve to ' + + "the same value `'state_changed'` at runtime.", + }, + { + name: 'TaskState', + reason: + 'The firebase-js-sdk declares `TaskState` as a string literal union. RN Firebase ' + + 'declares it as a const object and derives the type via ' + + '`(typeof TaskState)[keyof typeof TaskState]`. The RN const additionally includes ' + + "a `CANCELLED` alias (with double-L) for `'canceled'` for backwards compatibility.", + }, + ], +}; + +export default config; diff --git a/.github/scripts/compare-types/packages/storage/storage-js-sdk.d.ts b/.github/scripts/compare-types/packages/storage/storage-js-sdk.d.ts new file mode 100644 index 0000000000..83d657bc0d --- /dev/null +++ b/.github/scripts/compare-types/packages/storage/storage-js-sdk.d.ts @@ -0,0 +1,725 @@ +/** + * Cloud Storage for Firebase + * + * @packageDocumentation + */ +import { CompleteFn , EmulatorMockTokenOptions , FirebaseError , NextFn , Subscribe , Unsubscribe } from '@firebase/util'; +import { FirebaseApp } from '@firebase/app'; +/** + * Modify this {@link FirebaseStorage} instance to communicate with the Cloud Storage emulator. + * + * @param storage - The {@link FirebaseStorage} instance + * @param host - The emulator host (ex: localhost) + * @param port - The emulator port (ex: 5001) + * @param options - Emulator options. `options.mockUserToken` is the mock auth + * token to use for unit testing Security Rules. + * @public + */ +export declare function connectStorageEmulator(storage: FirebaseStorage, host: string, port: number, options?: { + mockUserToken?: EmulatorMockTokenOptions | string; +}): void; +/* Excluded from this release type: _dataFromString */ +/** + * Deletes the object at this location. + * @public + * @param ref - {@link StorageReference} for object to delete. + * @returns A `Promise` that resolves if the deletion succeeds. + */ +export declare function deleteObject(ref: StorageReference): Promise; +export { EmulatorMockTokenOptions }; +/* Excluded from this release type: _FbsBlob */ +/* Excluded from this release type: _FirebaseService */ +/** + * A Firebase Storage instance. + * @public + */ +export declare interface FirebaseStorage { + /** + * The {@link @firebase/app#FirebaseApp} associated with this `FirebaseStorage` instance. + */ + readonly app: FirebaseApp; + /** + * The maximum time to retry uploads in milliseconds. + */ + maxUploadRetryTime: number; + /** + * The maximum time to retry operations other than uploads or downloads in + * milliseconds. + */ + maxOperationRetryTime: number; +} +/* Excluded from this release type: _FirebaseStorageImpl */ +/** + * The full set of object metadata, including read-only properties. + * @public + */ +export declare interface FullMetadata extends UploadMetadata { + /** + * The bucket this object is contained in. + */ + bucket: string; + /** + * The full path of this object. + */ + fullPath: string; + /** + * The object's generation. + * {@link https://cloud.google.com/storage/docs/metadata#generation-number} + */ + generation: string; + /** + * The object's metageneration. + * {@link https://cloud.google.com/storage/docs/metadata#generation-number} + */ + metageneration: string; + /** + * The short name of this object, which is the last component of the full path. + * For example, if fullPath is 'full/path/image.png', name is 'image.png'. + */ + name: string; + /** + * The size of this object, in bytes. + */ + size: number; + /** + * A date string representing when this object was created. + */ + timeCreated: string; + /** + * A date string representing when this object was last updated. + */ + updated: string; + /** + * Tokens to allow access to the download URL. + */ + downloadTokens: string[] | undefined; + /** + * `StorageReference` associated with this upload. + */ + ref?: StorageReference | undefined; +} +/** + * Downloads the data at the object's location. Returns an error if the object + * is not found. + * + * To use this functionality, you have to whitelist your app's origin in your + * Cloud Storage bucket. See also + * https://cloud.google.com/storage/docs/configuring-cors + * + * This API is not available in Node. + * + * @public + * @param ref - StorageReference where data should be downloaded. + * @param maxDownloadSizeBytes - If set, the maximum allowed size in bytes to + * retrieve. + * @returns A Promise that resolves with a Blob containing the object's bytes + */ +export declare function getBlob(ref: StorageReference, maxDownloadSizeBytes?: number): Promise; +/** + * Downloads the data at the object's location. Returns an error if the object + * is not found. + * + * To use this functionality, you have to whitelist your app's origin in your + * Cloud Storage bucket. See also + * https://cloud.google.com/storage/docs/configuring-cors + * + * @public + * @param ref - StorageReference where data should be downloaded. + * @param maxDownloadSizeBytes - If set, the maximum allowed size in bytes to + * retrieve. + * @returns A Promise containing the object's bytes + */ +export declare function getBytes(ref: StorageReference, maxDownloadSizeBytes?: number): Promise; +/* Excluded from this release type: _getChild */ +/** + * Returns the download URL for the given {@link StorageReference}. + * @public + * @param ref - {@link StorageReference} to get the download URL for. + * @returns A `Promise` that resolves with the download + * URL for this object. + */ +export declare function getDownloadURL(ref: StorageReference): Promise; +/** + * A `Promise` that resolves with the metadata for this object. If this + * object doesn't exist or metadata cannot be retrieved, the promise is + * rejected. + * @public + * @param ref - {@link StorageReference} to get metadata from. + */ +export declare function getMetadata(ref: StorageReference): Promise; +/** + * Gets a {@link FirebaseStorage} instance for the given Firebase app. + * @public + * @param app - Firebase app to get {@link FirebaseStorage} instance for. + * @param bucketUrl - The gs:// url to your Firebase Storage Bucket. + * If not passed, uses the app's default Storage Bucket. + * @returns A {@link FirebaseStorage} instance. + */ +export declare function getStorage(app?: FirebaseApp, bucketUrl?: string): FirebaseStorage; +/** + * Downloads the data at the object's location. Raises an error event if the + * object is not found. + * + * This API is only available in Node. + * + * @public + * @param ref - StorageReference where data should be downloaded. + * @param maxDownloadSizeBytes - If set, the maximum allowed size in bytes to + * retrieve. + * @returns A stream with the object's data as bytes + */ +export declare function getStream(ref: StorageReference, maxDownloadSizeBytes?: number): ReadableStream; +/* Excluded from this release type: _invalidArgument */ +/* Excluded from this release type: _invalidRootOperation */ +/** + * List items (files) and prefixes (folders) under this storage reference. + * + * List API is only available for Firebase Rules Version 2. + * + * GCS is a key-blob store. Firebase Storage imposes the semantic of '/' + * delimited folder structure. + * Refer to GCS's List API if you want to learn more. + * + * To adhere to Firebase Rules's Semantics, Firebase Storage does not + * support objects whose paths end with "/" or contain two consecutive + * "/"s. Firebase Storage List API will filter these unsupported objects. + * list() may fail if there are too many unsupported objects in the bucket. + * @public + * + * @param ref - {@link StorageReference} to get list from. + * @param options - See {@link ListOptions} for details. + * @returns A `Promise` that resolves with the items and prefixes. + * `prefixes` contains references to sub-folders and `items` + * contains references to objects in this folder. `nextPageToken` + * can be used to get the rest of the results. + */ +export declare function list(ref: StorageReference, options?: ListOptions): Promise; +/** + * List all items (files) and prefixes (folders) under this storage reference. + * + * This is a helper method for calling list() repeatedly until there are + * no more results. The default pagination size is 1000. + * + * Note: The results may not be consistent if objects are changed while this + * operation is running. + * + * Warning: `listAll` may potentially consume too many resources if there are + * too many results. + * @public + * @param ref - {@link StorageReference} to get list from. + * + * @returns A `Promise` that resolves with all the items and prefixes under + * the current storage reference. `prefixes` contains references to + * sub-directories and `items` contains references to objects in this + * folder. `nextPageToken` is never returned. + */ +export declare function listAll(ref: StorageReference): Promise; +/** + * The options `list()` accepts. + * @public + */ +export declare interface ListOptions { + /** + * If set, limits the total number of `prefixes` and `items` to return. + * The default and maximum maxResults is 1000. + */ + maxResults?: number | null; + /** + * The `nextPageToken` from a previous call to `list()`. If provided, + * listing is resumed from the previous position. + */ + pageToken?: string | null; +} +/** + * Result returned by list(). + * @public + */ +export declare interface ListResult { + /** + * References to prefixes (sub-folders). You can call list() on them to + * get its contents. + * + * Folders are implicit based on '/' in the object paths. + * For example, if a bucket has two objects '/a/b/1' and '/a/b/2', list('/a') + * will return '/a/b' as a prefix. + */ + prefixes: StorageReference[]; + /** + * Objects in this directory. + * You can call getMetadata() and getDownloadUrl() on them. + */ + items: StorageReference[]; + /** + * If set, there might be more results for this list. Use this token to resume the list. + */ + nextPageToken?: string; +} +/** + * Returns a {@link StorageReference} for the given url. + * @param storage - {@link FirebaseStorage} instance. + * @param url - URL. If empty, returns root reference. + * @public + */ +export declare function ref(storage: FirebaseStorage, url?: string): StorageReference; +/** + * Returns a {@link StorageReference} for the given path in the + * default bucket. + * @param storageOrRef - {@link FirebaseStorage} or {@link StorageReference}. + * @param pathOrUrlStorage - path. If empty, returns root reference (if {@link FirebaseStorage} + * instance provided) or returns same reference (if {@link StorageReference} provided). + * @public + */ +export declare function ref(storageOrRef: FirebaseStorage | StorageReference, path?: string): StorageReference; +/** + * Object metadata that can be set at any time. + * @public + */ +export declare interface SettableMetadata { + /** + * Served as the 'Cache-Control' header on object download. + */ + cacheControl?: string | undefined; + /** + * Served as the 'Content-Disposition' header on object download. + */ + contentDisposition?: string | undefined; + /** + * Served as the 'Content-Encoding' header on object download. + */ + contentEncoding?: string | undefined; + /** + * Served as the 'Content-Language' header on object download. + */ + contentLanguage?: string | undefined; + /** + * Served as the 'Content-Type' header on object download. + */ + contentType?: string | undefined; + /** + * Additional user-defined custom metadata. + */ + customMetadata?: { + [key: string]: string; + } | undefined; +} +/** + * An error returned by the Firebase Storage SDK. + * @public + */ +export declare class StorageError extends FirebaseError { + private status_; + /** + * Stores custom error data unique to the `StorageError`. + */ + customData: { + serverResponse: string | null; + }; + /** + * @param code - A `StorageErrorCode` string to be prefixed with 'storage/' and + * added to the end of the message. + * @param message - Error message. + * @param status_ - Corresponding HTTP Status Code + */ + constructor(code: StorageErrorCode, message: string, status_?: number); + get status(): number; + set status(status: number); + /** + * Optional response message that was added by the server. + */ + get serverResponse(): null | string; + set serverResponse(serverResponse: string | null); +} +/** + * @public + * Error codes that can be attached to `StorageError` objects. + */ +export declare enum StorageErrorCode { + UNKNOWN = "unknown", + OBJECT_NOT_FOUND = "object-not-found", + BUCKET_NOT_FOUND = "bucket-not-found", + PROJECT_NOT_FOUND = "project-not-found", + QUOTA_EXCEEDED = "quota-exceeded", + UNAUTHENTICATED = "unauthenticated", + UNAUTHORIZED = "unauthorized", + UNAUTHORIZED_APP = "unauthorized-app", + RETRY_LIMIT_EXCEEDED = "retry-limit-exceeded", + INVALID_CHECKSUM = "invalid-checksum", + CANCELED = "canceled", + INVALID_EVENT_NAME = "invalid-event-name", + INVALID_URL = "invalid-url", + INVALID_DEFAULT_BUCKET = "invalid-default-bucket", + NO_DEFAULT_BUCKET = "no-default-bucket", + CANNOT_SLICE_BLOB = "cannot-slice-blob", + SERVER_FILE_WRONG_SIZE = "server-file-wrong-size", + NO_DOWNLOAD_URL = "no-download-url", + INVALID_ARGUMENT = "invalid-argument", + INVALID_ARGUMENT_COUNT = "invalid-argument-count", + APP_DELETED = "app-deleted", + INVALID_ROOT_OPERATION = "invalid-root-operation", + INVALID_FORMAT = "invalid-format", + INTERNAL_ERROR = "internal-error", + UNSUPPORTED_ENVIRONMENT = "unsupported-environment" +} +/** + * A stream observer for Firebase Storage. + * @public + */ +export declare interface StorageObserver { + next?: NextFn | null; + error?: (error: StorageError) => void | null; + complete?: CompleteFn | null; +} +/** + * Represents a reference to a Google Cloud Storage object. Developers can + * upload, download, and delete objects, as well as get/set object metadata. + * @public + */ +export declare interface StorageReference { + /** + * Returns a gs:// URL for this object in the form + * `gs://///` + * @returns The gs:// URL. + */ + toString(): string; + /** + * A reference to the root of this object's bucket. + */ + root: StorageReference; + /** + * The name of the bucket containing this reference's object. + */ + bucket: string; + /** + * The full path of this object. + */ + fullPath: string; + /** + * The short name of this object, which is the last component of the full path. + * For example, if fullPath is 'full/path/image.png', name is 'image.png'. + */ + name: string; + /** + * The {@link FirebaseStorage} instance associated with this reference. + */ + storage: FirebaseStorage; + /** + * A reference pointing to the parent location of this reference, or null if + * this reference is the root. + */ + parent: StorageReference | null; +} +/** + * @license + * Copyright 2017 Google LLC + * + * 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. + */ +/** + * An enumeration of the possible string formats for upload. + * @public + */ +export declare type StringFormat = (typeof StringFormat)[keyof typeof StringFormat]; +/** + * An enumeration of the possible string formats for upload. + * @public + */ +export declare const StringFormat: { + /** + * Indicates the string should be interpreted "raw", that is, as normal text. + * The string will be interpreted as UTF-16, then uploaded as a UTF-8 byte + * sequence. + * Example: The string 'Hello! \\ud83d\\ude0a' becomes the byte sequence + * 48 65 6c 6c 6f 21 20 f0 9f 98 8a + */ + readonly RAW: "raw"; + /** + * Indicates the string should be interpreted as base64-encoded data. + * Padding characters (trailing '='s) are optional. + * Example: The string 'rWmO++E6t7/rlw==' becomes the byte sequence + * ad 69 8e fb e1 3a b7 bf eb 97 + */ + readonly BASE64: "base64"; + /** + * Indicates the string should be interpreted as base64url-encoded data. + * Padding characters (trailing '='s) are optional. + * Example: The string 'rWmO--E6t7_rlw==' becomes the byte sequence + * ad 69 8e fb e1 3a b7 bf eb 97 + */ + readonly BASE64URL: "base64url"; + /** + * Indicates the string is a data URL, such as one obtained from + * canvas.toDataURL(). + * Example: the string 'data:application/octet-stream;base64,aaaa' + * becomes the byte sequence + * 69 a6 9a + * (the content-type "application/octet-stream" is also applied, but can + * be overridden in the metadata object). + */ + readonly DATA_URL: "data_url"; +}; +/** + * An event that is triggered on a task. + * @public + */ +export declare type TaskEvent = 'state_changed'; +/* Excluded from this release type: _TaskEvent */ +/** + * Represents the current state of a running upload. + * @public + */ +export declare type TaskState = 'running' | 'paused' | 'success' | 'canceled' | 'error'; +/** + * Updates the metadata for this object. + * @public + * @param ref - {@link StorageReference} to update metadata for. + * @param metadata - The new metadata for the object. + * Only values that have been explicitly set will be changed. Explicitly + * setting a value to null will remove the metadata. + * @returns A `Promise` that resolves with the new metadata for this object. + */ +export declare function updateMetadata(ref: StorageReference, metadata: SettableMetadata): Promise; +/** + * Uploads data to this object's location. + * The upload is not resumable. + * @public + * @param ref - {@link StorageReference} where data should be uploaded. + * @param data - The data to upload. + * @param metadata - Metadata for the data to upload. + * @returns A Promise containing an UploadResult + */ +export declare function uploadBytes(ref: StorageReference, data: Blob | Uint8Array | ArrayBuffer, metadata?: UploadMetadata): Promise; +/** + * Uploads data to this object's location. + * The upload can be paused and resumed, and exposes progress updates. + * @public + * @param ref - {@link StorageReference} where data should be uploaded. + * @param data - The data to upload. + * @param metadata - Metadata for the data to upload. + * @returns An UploadTask + */ +export declare function uploadBytesResumable(ref: StorageReference, data: Blob | Uint8Array | ArrayBuffer, metadata?: UploadMetadata): UploadTask; +/** + * Object metadata that can be set at upload. + * @public + */ +export declare interface UploadMetadata extends SettableMetadata { + /** + * A Base64-encoded MD5 hash of the object being uploaded. + */ + md5Hash?: string | undefined; +} +/** + * Result returned from a non-resumable upload. + * @public + */ +export declare interface UploadResult { + /** + * Contains the metadata sent back from the server. + */ + readonly metadata: FullMetadata; + /** + * The reference that spawned this upload. + */ + readonly ref: StorageReference; +} +/** + * Uploads a string to this object's location. + * The upload is not resumable. + * @public + * @param ref - {@link StorageReference} where string should be uploaded. + * @param value - The string to upload. + * @param format - The format of the string to upload. + * @param metadata - Metadata for the string to upload. + * @returns A Promise containing an UploadResult + */ +export declare function uploadString(ref: StorageReference, value: string, format?: StringFormat, metadata?: UploadMetadata): Promise; +/** + * Represents the process of uploading an object. Allows you to monitor and + * manage the upload. + * @public + */ +export declare interface UploadTask { + /** + * Cancels a running task. Has no effect on a complete or failed task. + * @returns True if the cancel had an effect. + */ + cancel(): boolean; + /** + * Equivalent to calling `then(null, onRejected)`. + */ + catch(onRejected: (error: StorageError) => unknown): Promise; + /** + * Listens for events on this task. + * + * Events have three callback functions (referred to as `next`, `error`, and + * `complete`). + * + * If only the event is passed, a function that can be used to register the + * callbacks is returned. Otherwise, the callbacks are passed after the event. + * + * Callbacks can be passed either as three separate arguments or as the + * `next`, `error`, and `complete` properties of an object. Any of the three + * callbacks is optional, as long as at least one is specified. In addition, + * when you add your callbacks, you get a function back. You can call this + * function to unregister the associated callbacks. + * + * @example **Pass callbacks separately or in an object.** + * ```javascript + * var next = function(snapshot) {}; + * var error = function(error) {}; + * var complete = function() {}; + * + * // The first example. + * uploadTask.on( + * firebase.storage.TaskEvent.STATE_CHANGED, + * next, + * error, + * complete); + * + * // This is equivalent to the first example. + * uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED, { + * 'next': next, + * 'error': error, + * 'complete': complete + * }); + * + * // This is equivalent to the first example. + * var subscribe = uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED); + * subscribe(next, error, complete); + * + * // This is equivalent to the first example. + * var subscribe = uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED); + * subscribe({ + * 'next': next, + * 'error': error, + * 'complete': complete + * }); + * ``` + * + * @example **Any callback is optional.** + * ```javascript + * // Just listening for completion, this is legal. + * uploadTask.on( + * firebase.storage.TaskEvent.STATE_CHANGED, + * null, + * null, + * function() { + * console.log('upload complete!'); + * }); + * + * // Just listening for progress/state changes, this is legal. + * uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED, function(snapshot) { + * var percent = snapshot.bytesTransferred / snapshot.totalBytes * 100; + * console.log(percent + "% done"); + * }); + * + * // This is also legal. + * uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED, { + * 'complete': function() { + * console.log('upload complete!'); + * } + * }); + * ``` + * + * @example **Use the returned function to remove callbacks.** + * ```javascript + * var unsubscribe = uploadTask.on( + * firebase.storage.TaskEvent.STATE_CHANGED, + * function(snapshot) { + * var percent = snapshot.bytesTransferred / snapshot.totalBytes * 100; + * console.log(percent + "% done"); + * // Stop after receiving one update. + * unsubscribe(); + * }); + * + * // This code is equivalent to the above. + * var handle = uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED); + * unsubscribe = handle(function(snapshot) { + * var percent = snapshot.bytesTransferred / snapshot.totalBytes * 100; + * console.log(percent + "% done"); + * // Stop after receiving one update. + * unsubscribe(); + * }); + * ``` + * + * @param event - The type of event to listen for. + * @param nextOrObserver - + * The `next` function, which gets called for each item in + * the event stream, or an observer object with some or all of these three + * properties (`next`, `error`, `complete`). + * @param error - A function that gets called with a `StorageError` + * if the event stream ends due to an error. + * @param completed - A function that gets called if the + * event stream ends normally. + * @returns + * If only the event argument is passed, returns a function you can use to + * add callbacks (see the examples above). If more than just the event + * argument is passed, returns a function you can call to unregister the + * callbacks. + */ + on(event: TaskEvent, nextOrObserver?: StorageObserver | null | ((snapshot: UploadTaskSnapshot) => unknown), error?: ((a: StorageError) => unknown) | null, complete?: Unsubscribe | null): Unsubscribe | Subscribe; + /** + * Pauses a currently running task. Has no effect on a paused or failed task. + * @returns True if the operation took effect, false if ignored. + */ + pause(): boolean; + /** + * Resumes a paused task. Has no effect on a currently running or failed task. + * @returns True if the operation took effect, false if ignored. + */ + resume(): boolean; + /** + * A snapshot of the current task state. + */ + snapshot: UploadTaskSnapshot; + /** + * This object behaves like a Promise, and resolves with its snapshot data + * when the upload completes. + * @param onFulfilled - The fulfillment callback. Promise chaining works as normal. + * @param onRejected - The rejection callback. + */ + then(onFulfilled?: ((snapshot: UploadTaskSnapshot) => unknown) | null, onRejected?: ((error: StorageError) => unknown) | null): Promise; +} +/* Excluded from this release type: _UploadTask */ +/** + * Holds data about the current state of the upload task. + * @public + */ +export declare interface UploadTaskSnapshot { + /** + * The number of bytes that have been successfully uploaded so far. + */ + bytesTransferred: number; + /** + * Before the upload completes, contains the metadata sent to the server. + * After the upload completes, contains the metadata sent back from the server. + */ + metadata: FullMetadata; + /** + * The reference that spawned this snapshot's upload task. + */ + ref: StorageReference; + /** + * The current state of the task. + */ + state: TaskState; + /** + * The task of which this is a snapshot. + */ + task: UploadTask; + /** + * The total number of bytes to be uploaded. + */ + totalBytes: number; +} +export {}; diff --git a/.github/scripts/compare-types/src/registry.ts b/.github/scripts/compare-types/src/registry.ts index d05982f7b4..3d31390a07 100644 --- a/.github/scripts/compare-types/src/registry.ts +++ b/.github/scripts/compare-types/src/registry.ts @@ -13,6 +13,7 @@ import path from 'path'; import type { PackageConfig } from './types'; +import storageConfig from '../packages/storage/config'; import aiConfig from '../packages/ai/config'; import firestoreConfig from '../packages/firestore/config'; import firestorePipelinesConfig from '../packages/firestore-pipelines/config'; @@ -71,6 +72,25 @@ export const packages: PackageEntry[] = [ // ], // config: remoteConfigConfig, // }, + { + name: 'storage', + firebaseSdkTypesPaths: [path.join( + SCRIPT_DIR, + 'packages', + 'storage', + 'storage-js-sdk.d.ts', + )], + rnFirebaseModularFiles: [ + path.join(rnDist('storage'), 'types', 'storage.d.ts'), + path.join(rnDist('storage'), 'modular.d.ts'), + ], + rnFirebaseSupportFiles: [ + path.join(rnDist('storage'), 'StorageStatics.d.ts'), + path.join(rnDist('storage'), 'types', 'namespaced.d.ts'), + path.join(rnDist('storage'), 'types', 'internal.d.ts'), + ], + config: storageConfig, + }, { name: 'ai', firebaseSdkTypesPaths: [ @@ -177,3 +197,5 @@ export const packages: PackageEntry[] = [ config: firestorePipelinesConfig, }, ]; + + diff --git a/packages/app/lib/common/index.ts b/packages/app/lib/common/index.ts index 9c6753cb8b..cbd99229d2 100644 --- a/packages/app/lib/common/index.ts +++ b/packages/app/lib/common/index.ts @@ -510,7 +510,7 @@ const mapOfDeprecationReplacements: DeprecationMap = { setMaxUploadRetryTime: 'setMaxUploadRetryTime()', setMaxDownloadRetryTime: 'setMaxDownloadRetryTime()', }, - StorageReference: { + Reference: { delete: 'deleteObject()', getDownloadURL: 'getDownloadURL()', getMetadata: 'getMetadata()', @@ -602,7 +602,7 @@ function getNamespace(target: any): string | undefined { if (target._config && target._config.namespace) { return target._config.namespace; } - if (target.constructor.name === 'StorageReference') { + if (target.constructor.name === 'Reference') { return 'storage'; } const className = target.name ? target.name : target.constructor.name; @@ -627,7 +627,7 @@ function getInstanceName(target: any): string { return 'default'; } - if (target.constructor.name === 'StorageReference') { + if (target.constructor.name === 'Reference') { // if path passed into ref(), it will pass in the arg as target.name return target.constructor.name; } @@ -639,6 +639,31 @@ function getInstanceName(target: any): string { return target.constructor.name; } +const MODULAR_INSTANCE_SYMBOL = Symbol('react-native-firebase-modular-instance'); + +function isObjectLike(value: unknown): value is object | ((...args: any[]) => unknown) { + return (typeof value === 'object' && value !== null) || typeof value === 'function'; +} + +function isModularInstance(value: unknown): boolean { + return isObjectLike(value) && (value as any)[MODULAR_INSTANCE_SYMBOL] === true; +} + +function markModularInstance(value: T): T { + if (!isObjectLike(value) || isModularInstance(value)) { + return value; + } + + Object.defineProperty(value, MODULAR_INSTANCE_SYMBOL, { + value: true, + configurable: true, + enumerable: false, + writable: false, + }); + + return value; +} + export function createDeprecationProxy(instance: T): T { return new Proxy(instance, { construct(target: any, args: any[]) { @@ -647,13 +672,14 @@ export function createDeprecationProxy(instance: T): T { }, get(target: any, prop: string | symbol, receiver: any) { const originalMethod = target[prop]; + const modularAccess = _isModularCall || isModularInstance(target); if (prop === 'constructor') { return Reflect.get(target, prop, receiver); } if (target && target.constructor && target.constructor.name === 'Timestamp') { - deprecationConsoleWarning('firestore', prop as string, 'Timestamp', false); + deprecationConsoleWarning('firestore', prop as string, 'Timestamp', modularAccess); return Reflect.get(target, prop, receiver); } @@ -725,14 +751,15 @@ export function createDeprecationProxy(instance: T): T { if (descriptor.get && nameSpace) { // Handle getter - call it and show deprecation warning - deprecationConsoleWarning(nameSpace, prop as string, instanceName, _isModularCall); - return descriptor.get.call(target); + deprecationConsoleWarning(nameSpace, prop as string, instanceName, modularAccess); + const result = descriptor.get.call(target); + return modularAccess ? markModularInstance(result) : result; } if (descriptor.set && nameSpace) { // Handle setter - return a function that calls the setter with deprecation warning return function (value: any) { - deprecationConsoleWarning(nameSpace, prop as string, instanceName, _isModularCall); + deprecationConsoleWarning(nameSpace, prop as string, instanceName, modularAccess); descriptor.set!.call(target, value); }; } @@ -740,7 +767,7 @@ export function createDeprecationProxy(instance: T): T { if (typeof originalMethod === 'function') { return function (...args: any[]) { - const isModularMethod = args.includes(MODULAR_DEPRECATION_ARG); + const isModularMethod = modularAccess || args.includes(MODULAR_DEPRECATION_ARG); const instanceName = getInstanceName(target); const nameSpace = getNamespace(target); @@ -748,7 +775,8 @@ export function createDeprecationProxy(instance: T): T { deprecationConsoleWarning(nameSpace, prop as string, instanceName, isModularMethod); } - return originalMethod.apply(target, filterModularArgument(args)); + const result = originalMethod.apply(target, filterModularArgument(args)); + return isModularMethod ? markModularInstance(result) : result; }; } return Reflect.get(target, prop, receiver); @@ -796,3 +824,8 @@ export function warnIfNotModularCall(args: IArguments, replacementMethodName: st } } } + +export function isModularCall(args: IArguments): boolean { + const argsArray = Array.from(args); + return argsArray.includes(MODULAR_DEPRECATION_ARG); +} diff --git a/packages/app/lib/common/unitTestUtils.ts b/packages/app/lib/common/unitTestUtils.ts index f2315f06f8..3011293786 100644 --- a/packages/app/lib/common/unitTestUtils.ts +++ b/packages/app/lib/common/unitTestUtils.ts @@ -22,6 +22,16 @@ export type CheckV9DeprecationFunction = ( ignoreFirebaseAppDeprecationWarning?: boolean, ) => void; +export const withDeprecationWarningsSilenced = (fn: () => T): T => { + const previous = globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS; + globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; + try { + return fn(); + } finally { + globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = previous; + } +}; + export const createCheckV9Deprecation = (moduleNames: string[]): CheckV9DeprecationFunction => { return ( modularFunction: () => void, diff --git a/packages/app/lib/types/common.ts b/packages/app/lib/types/common.ts new file mode 100644 index 0000000000..8eae14d815 --- /dev/null +++ b/packages/app/lib/types/common.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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. + * + */ + +export type NextFn = (value: T) => void; +export type ErrorFn = (error: Error) => void; +export type CompleteFn = () => void; + +export interface Observer { + next: NextFn; + error: ErrorFn; + complete: CompleteFn; +} + +export type PartialObserver = Partial>; + +/** + * A function that unsubscribes from an event listener. + */ +export type Unsubscribe = () => void; diff --git a/packages/database/__tests__/database.test.ts b/packages/database/__tests__/database.test.ts index 4d61a55903..cb29dca14e 100644 --- a/packages/database/__tests__/database.test.ts +++ b/packages/database/__tests__/database.test.ts @@ -52,6 +52,7 @@ import database, { import { createCheckV9Deprecation, CheckV9DeprecationFunction, + withDeprecationWarningsSilenced, } from '../../app/lib/common/unitTestUtils'; import FirebaseModule from '../../app/lib/internal/FirebaseModule'; @@ -450,9 +451,10 @@ describe('Database', function () { it('child', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => child(testRef, 'child'), - () => testRef.child('child'), + () => testRef2.child('child'), 'child', ); }); @@ -460,9 +462,10 @@ describe('Database', function () { it('set', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => set(testRef, 'value'), - () => testRef.set('value'), + () => testRef2.set('value'), 'set', ); }); @@ -470,9 +473,10 @@ describe('Database', function () { it('update', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => update(testRef, { value: 'value' }), - () => testRef.update({ value: 'value' }), + () => testRef2.update({ value: 'value' }), 'update', ); }); @@ -480,9 +484,10 @@ describe('Database', function () { it('setWithPriority', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => setWithPriority(testRef, 'value', 1), - () => testRef.setWithPriority('value', 1), + () => testRef2.setWithPriority('value', 1), 'setWithPriority', ); }); @@ -490,9 +495,10 @@ describe('Database', function () { it('remove', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => remove(testRef), - () => testRef.remove(), + () => testRef2.remove(), 'remove', ); }); @@ -500,9 +506,10 @@ describe('Database', function () { it('onValue', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => onValue(testRef, () => {}), - () => testRef.on('value', () => {}), + () => testRef2.on('value', () => {}), 'on', ); }); @@ -510,9 +517,10 @@ describe('Database', function () { it('get', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => get(testRef), - () => testRef.once('value'), + () => testRef2.once('value'), 'once', ); }); @@ -520,9 +528,10 @@ describe('Database', function () { it('endAt', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => query(testRef, endAt('value')), - () => testRef.endAt('value'), + () => testRef2.endAt('value'), 'endAt', ); }); @@ -530,9 +539,10 @@ describe('Database', function () { it('startAt', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => query(testRef, startAt('value')), - () => testRef.startAt('value'), + () => testRef2.startAt('value'), 'startAt', ); }); @@ -540,9 +550,10 @@ describe('Database', function () { it('limitToFirst', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => query(testRef, limitToFirst(10)), - () => testRef.limitToFirst(10), + () => testRef2.limitToFirst(10), 'limitToFirst', ); }); @@ -550,9 +561,10 @@ describe('Database', function () { it('limitToLast', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => query(testRef, limitToLast(10)), - () => testRef.limitToLast(10), + () => testRef2.limitToLast(10), 'limitToLast', ); }); @@ -560,9 +572,10 @@ describe('Database', function () { it('orderByChild', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => query(testRef, orderByChild('name')), - () => testRef.orderByChild('name'), + () => testRef2.orderByChild('name'), 'orderByChild', ); }); @@ -570,9 +583,10 @@ describe('Database', function () { it('orderByKey', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => query(testRef, orderByKey()), - () => testRef.orderByKey(), + () => testRef2.orderByKey(), 'orderByKey', ); }); @@ -580,9 +594,10 @@ describe('Database', function () { it('orderByValue', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => query(testRef, orderByValue()), - () => testRef.orderByValue(), + () => testRef2.orderByValue(), 'orderByValue', ); }); @@ -590,9 +605,10 @@ describe('Database', function () { it('equalTo', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => query(testRef, equalTo('value')), - () => testRef.equalTo('value'), + () => testRef2.equalTo('value'), 'equalTo', ); }); @@ -600,9 +616,10 @@ describe('Database', function () { it('setPriority', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => setPriority(testRef, 'value'), - () => testRef.setPriority('value'), + () => testRef2.setPriority('value'), 'setPriority', ); }); @@ -610,9 +627,10 @@ describe('Database', function () { it('push', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => push(testRef, 'value'), - () => testRef.push('value'), + () => testRef2.push('value'), 'push', ); }); @@ -620,9 +638,10 @@ describe('Database', function () { it('onDisconnect', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => onDisconnect(testRef), - () => testRef.onDisconnect(), + () => testRef2.onDisconnect(), 'onDisconnect', ); }); @@ -630,9 +649,10 @@ describe('Database', function () { it('keepSynced', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => keepSynced(testRef, true), - () => testRef.keepSynced(true), + () => testRef2.keepSynced(true), 'keepSynced', ); }); @@ -642,9 +662,10 @@ describe('Database', function () { it('runTransaction', function () { const db = getDatabase(); const testRef = ref(db, 'test'); + const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); referenceV9Deprecation( () => runTransaction(testRef, currentData => currentData, { applyLocally: true }), - () => testRef.transaction(currentData => currentData, undefined, true), + () => testRef2.transaction((currentData: any) => currentData, undefined, true), 'transaction', ); }); diff --git a/packages/firestore/__tests__/firestore.test.ts b/packages/firestore/__tests__/firestore.test.ts index 496dd80ea1..e46e19d757 100644 --- a/packages/firestore/__tests__/firestore.test.ts +++ b/packages/firestore/__tests__/firestore.test.ts @@ -13,6 +13,7 @@ import * as nativeModule from '@react-native-firebase/app/dist/module/internal/n import { createCheckV9Deprecation, CheckV9DeprecationFunction, + withDeprecationWarningsSilenced, } from '../../app/lib/common/unitTestUtils'; import { getApp } from '../../app/lib/modular'; @@ -973,11 +974,13 @@ describe('Firestore', function () { const firestore = getFirestore(); const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => getCountFromServer(query), - // @ts-expect-error Combines modular and namespace API - () => query.count(), + () => query2.count(), 'count', ); }); @@ -986,37 +989,37 @@ describe('Firestore', function () { const firestore = getFirestore(); const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => getCountFromServer(query), - // @ts-expect-error Combines modular and namespace API - () => query.countFromServer(), + () => query2.countFromServer(), 'countFromServer', ); }); it('CollectionReference.endAt()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => endAt('foo'), - // @ts-expect-error Combines modular and namespace API - () => query.endAt('foo'), + () => query2.endAt('foo'), 'endAt', ); }); it('CollectionReference.endBefore()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => endBefore('foo'), - // @ts-expect-error Combines modular and namespace API - () => query.endBefore('foo'), + () => query2.endBefore('foo'), 'endBefore', ); }); @@ -1025,51 +1028,50 @@ describe('Firestore', function () { const firestore = getFirestore(); const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => getDocs(query), - // @ts-expect-error Combines modular and namespace API - () => query.get(), + () => query2.get(), 'get', ); }); it('CollectionReference.isEqual()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( // no equivalent method () => {}, - // @ts-expect-error Combines modular and namespace API - () => query.isEqual(query), + () => query2.isEqual(query2), 'isEqual', ); }); it('CollectionReference.limit()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => limit(9), - // @ts-expect-error Combines modular and namespace API - () => query.limit(9), + () => query2.limit(9), 'limit', ); }); it('CollectionReference.limitToLast()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => limitToLast(9), - // @ts-expect-error Combines modular and namespace API - () => query.limitToLast(9), + () => query2.limitToLast(9), 'limitToLast', ); }); @@ -1078,63 +1080,61 @@ describe('Firestore', function () { const firestore = getFirestore(); const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => onSnapshot(query, () => {}), - // @ts-expect-error Combines modular and namespace API - () => query.onSnapshot(() => {}), + () => query2.onSnapshot(() => {}), 'onSnapshot', ); }); it('CollectionReference.orderBy()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => orderBy('foo', 'asc'), - // @ts-expect-error Combines modular and namespace API - () => query.orderBy('foo', 'asc'), + () => query2.orderBy('foo', 'asc'), 'orderBy', ); }); it('CollectionReference.startAfter()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => startAfter('foo'), - // @ts-expect-error Combines modular and namespace API - () => query.startAfter('foo'), + () => query2.startAfter('foo'), 'startAfter', ); }); it('CollectionReference.startAt()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => startAt('foo'), - // @ts-expect-error Combines modular and namespace API - () => query.startAt('foo'), + () => query2.startAt('foo'), 'startAt', ); }); it('CollectionReference.where()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => where('foo', '==', 'bar'), - // @ts-expect-error Combines modular and namespace API - () => query.where('foo', '==', 'bar'), + () => query2.where('foo', '==', 'bar'), 'where', ); }); @@ -1143,11 +1143,13 @@ describe('Firestore', function () { const firestore = getFirestore(); const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => addDoc(query, { foo: 'bar' }), - // @ts-expect-error Combines modular and namespace API - () => query.add({ foo: 'bar' }), + () => query2.add({ foo: 'bar' }), 'add', ); }); @@ -1156,11 +1158,13 @@ describe('Firestore', function () { const firestore = getFirestore(); const query = collection(firestore, 'test'); + const query2 = withDeprecationWarningsSilenced(() => + firebase.firestore().collection('test'), + ); collectionRefV9Deprecation( () => doc(query, 'bar'), - // @ts-expect-error Combines modular and namespace API - () => query.doc('foo'), + () => query2.doc('foo'), 'doc', ); }); @@ -1169,13 +1173,11 @@ describe('Firestore', function () { describe('DocumentReference', function () { it('DocumentReference.collection()', function () { const firestore = getFirestore(); - - const docRef = doc(firestore, 'some/foo'); + const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); docRefV9Deprecation( () => collection(firestore, 'bar'), - // @ts-expect-error Combines modular and namespace API - () => docRef.collection('bar'), + () => docRef2.collection('bar'), 'collection', ); }); @@ -1184,11 +1186,11 @@ describe('Firestore', function () { const firestore = getFirestore(); const docRef = doc(firestore, 'some/foo'); + const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); docRefV9Deprecation( () => deleteDoc(docRef), - // @ts-expect-error Combines modular and namespace API - () => docRef.delete(), + () => docRef2.delete(), 'delete', ); }); @@ -1197,25 +1199,22 @@ describe('Firestore', function () { const firestore = getFirestore(); const docRef = doc(firestore, 'some/foo'); + const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); docRefV9Deprecation( () => getDoc(docRef), - // @ts-expect-error Combines modular and namespace API - () => docRef.get(), + () => docRef2.get(), 'get', ); }); it('DocumentReference.isEqual()', function () { - const firestore = getFirestore(); - - const docRef = doc(firestore, 'some/foo'); + const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); docRefV9Deprecation( // no equivalent method () => {}, - // @ts-expect-error Combines modular and namespace API - () => docRef.isEqual(docRef), + () => docRef2.isEqual(docRef2), 'isEqual', ); }); @@ -1224,11 +1223,11 @@ describe('Firestore', function () { const firestore = getFirestore(); const docRef = doc(firestore, 'some/foo'); + const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); docRefV9Deprecation( () => onSnapshot(docRef, () => {}), - // @ts-expect-error Combines modular and namespace API - () => docRef.onSnapshot(() => {}), + () => docRef2.onSnapshot(() => {}), 'onSnapshot', ); }); @@ -1237,11 +1236,11 @@ describe('Firestore', function () { const firestore = getFirestore(); const docRef = doc(firestore, 'some/foo'); + const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); docRefV9Deprecation( () => setDoc(docRef, { foo: 'bar' }), - // @ts-expect-error Combines modular and namespace API - () => docRef.set({ foo: 'bar' }), + () => docRef2.set({ foo: 'bar' }), 'set', ); }); @@ -1250,11 +1249,11 @@ describe('Firestore', function () { const firestore = getFirestore(); const docRef = doc(firestore, 'some/foo'); + const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); docRefV9Deprecation( () => updateDoc(docRef, { foo: 'bar' }), - // @ts-expect-error Combines modular and namespace API - () => docRef.update({ foo: 'bar' }), + () => docRef2.update({ foo: 'bar' }), 'update', ); }); @@ -1422,11 +1421,11 @@ describe('Firestore', function () { describe('FirestorePersistentCacheIndexManager', function () { it('firestore.persistentCacheIndexManager()', function () { const firestore = getFirestore(); + const firestore2 = withDeprecationWarningsSilenced(() => firebase.firestore()); firestoreRefV9Deprecation( () => getPersistentCacheIndexManager(firestore), - // @ts-expect-error Combines modular and namespace API - () => firestore.persistentCacheIndexManager(), + () => firestore2.persistentCacheIndexManager(), 'persistentCacheIndexManager', ); }); @@ -1435,11 +1434,15 @@ describe('Firestore', function () { const firestore = getFirestore(); // @ts-ignore test firestore._settings.persistence = true; - // @ts-expect-error Combines modular and namespace API - const indexManager = firestore.persistentCacheIndexManager(); + const firestore2 = withDeprecationWarningsSilenced(() => firebase.firestore()); + // @ts-ignore test + firestore2._settings.persistence = true; + const indexManager2 = withDeprecationWarningsSilenced( + () => firestore2.persistentCacheIndexManager()!, + ); persistentCacheIndexManagerV9Deprecation( - () => enablePersistentCacheIndexAutoCreation(indexManager!), - () => indexManager!.enableIndexAutoCreation(), + () => enablePersistentCacheIndexAutoCreation(getPersistentCacheIndexManager(firestore)!), + () => indexManager2.enableIndexAutoCreation(), 'enableIndexAutoCreation', ); }); @@ -1448,11 +1451,15 @@ describe('Firestore', function () { const firestore = getFirestore(); // @ts-ignore test firestore._settings.persistence = true; - // @ts-expect-error Combines modular and namespace API - const indexManager = firestore.persistentCacheIndexManager(); + const firestore2 = withDeprecationWarningsSilenced(() => firebase.firestore()); + // @ts-ignore test + firestore2._settings.persistence = true; + const indexManager2 = withDeprecationWarningsSilenced( + () => firestore2.persistentCacheIndexManager()!, + ); persistentCacheIndexManagerV9Deprecation( - () => disablePersistentCacheIndexAutoCreation(indexManager!), - () => indexManager!.disableIndexAutoCreation(), + () => disablePersistentCacheIndexAutoCreation(getPersistentCacheIndexManager(firestore)!), + () => indexManager2.disableIndexAutoCreation(), 'disableIndexAutoCreation', ); }); @@ -1461,11 +1468,15 @@ describe('Firestore', function () { const firestore = getFirestore(); // @ts-ignore test firestore._settings.persistence = true; - // @ts-expect-error Combines modular and namespace API - const indexManager = firestore.persistentCacheIndexManager(); + const firestore2 = withDeprecationWarningsSilenced(() => firebase.firestore()); + // @ts-ignore test + firestore2._settings.persistence = true; + const indexManager2 = withDeprecationWarningsSilenced( + () => firestore2.persistentCacheIndexManager()!, + ); persistentCacheIndexManagerV9Deprecation( - () => deleteAllPersistentCacheIndexes(indexManager!), - () => indexManager!.deleteAllIndexes(), + () => deleteAllPersistentCacheIndexes(getPersistentCacheIndexManager(firestore)!), + () => indexManager2.deleteAllIndexes(), 'deleteAllIndexes', ); }); @@ -1473,21 +1484,23 @@ describe('Firestore', function () { describe('Timestamp', function () { it('Timestamp.seconds', function () { - const timestamp = new firestore.Timestamp(2, 3); + const timestamp2 = withDeprecationWarningsSilenced( + () => new firebase.firestore.Timestamp(2, 3), + ); timestampV9Deprecation( - // no corresponding method - () => {}, - () => timestamp.seconds, + () => new Timestamp(2, 3).seconds, + () => timestamp2.seconds, 'seconds', ); }); it('Timestamp.nanoseconds', function () { - const timestamp = new firestore.Timestamp(2000, 3000000); + const timestamp2 = withDeprecationWarningsSilenced( + () => new firebase.firestore.Timestamp(2000, 3000000), + ); timestampV9Deprecation( - // no corresponding method - () => {}, - () => timestamp.nanoseconds, + () => new Timestamp(2000, 3000000).nanoseconds, + () => timestamp2.nanoseconds, 'nanoseconds', ); }); diff --git a/packages/storage/__tests__/storage.test.ts b/packages/storage/__tests__/storage.test.ts index 58faf37928..0c6b17acdc 100644 --- a/packages/storage/__tests__/storage.test.ts +++ b/packages/storage/__tests__/storage.test.ts @@ -17,13 +17,10 @@ import storage, { uploadBytes, uploadBytesResumable, uploadString, - refFromURL, setMaxOperationRetryTime, setMaxUploadRetryTime, putFile, writeToFile, - toString, - child, setMaxDownloadRetryTime, StringFormat, TaskEvent, @@ -33,6 +30,7 @@ import storage, { import { createCheckV9Deprecation, CheckV9DeprecationFunction, + withDeprecationWarningsSilenced, } from '../../app/lib/common/unitTestUtils'; // @ts-ignore test @@ -153,8 +151,12 @@ describe('Storage', function () { expect(uploadString).toBeDefined(); }); - it('`refFromURL` function is properly exposed to end user', function () { - expect(refFromURL).toBeDefined(); + it('`ref` supports full URLs (gs:// or https://) like firebase-js-sdk', function () { + expect(ref).toBeDefined(); + const storage = getStorage(); + const refFromUrl = ref(storage, 'gs://bucket/path/to/file'); + expect(refFromUrl).toBeDefined(); + expect(refFromUrl.fullPath).toBeDefined(); }); it('`setMaxOperationRetryTime` function is properly exposed to end user', function () { @@ -177,10 +179,6 @@ describe('Storage', function () { expect(toString).toBeDefined(); }); - it('`child` function is properly exposed to end user', function () { - expect(child).toBeDefined(); - }); - it('`setMaxDownloadRetryTime` function is properly exposed to end user', function () { expect(setMaxDownloadRetryTime).toBeDefined(); }); @@ -213,7 +211,7 @@ describe('Storage', function () { beforeEach(function () { storageV9Deprecation = createCheckV9Deprecation(['storage']); - storageRefV9Deprecation = createCheckV9Deprecation(['storage', 'StorageReference']); + storageRefV9Deprecation = createCheckV9Deprecation(['storage', 'Reference']); staticsV9Deprecation = createCheckV9Deprecation(['storage', 'statics']); @@ -250,6 +248,7 @@ describe('Storage', function () { const storage = getStorage(); storageV9Deprecation( () => connectStorageEmulator(storage, 'localhost', 8080), + // @ts-expect-error Combines modular and namespace API () => storage.useEmulator('localhost', 8080), 'useEmulator', ); @@ -259,163 +258,198 @@ describe('Storage', function () { const storage = getStorage(); storageV9Deprecation( () => ref(storage, 'foo'), + // @ts-expect-error Combines modular and namespace API () => storage.ref('foo'), 'ref', ); }); - it('refFromURL()', function () { - const storage = firebase.app().storage(); + it('ref() with full URL', function () { + const storage = getStorage(); storageV9Deprecation( - () => refFromURL(storage, 'gs://flutterfire-e2e-tests.appspot.com/flutter-tsts'), + () => ref(storage, 'gs://flutterfire-e2e-tests.appspot.com/flutter-tsts'), + // @ts-expect-error Combines modular and namespace API () => storage.refFromURL('gs://flutterfire-e2e-tests.appspot.com/flutter-tsts'), 'refFromURL', ); }); it('setMaxOperationRetryTime()', function () { - const storage = firebase.app().storage(); + const storage = getStorage(); storageV9Deprecation( () => setMaxOperationRetryTime(storage, 1000), + // @ts-expect-error Combines modular and namespace API () => storage.setMaxOperationRetryTime(1000), 'setMaxOperationRetryTime', ); }); it('setMaxUploadRetryTime()', function () { - const storage = firebase.app().storage(); + const storage = getStorage(); storageV9Deprecation( () => setMaxUploadRetryTime(storage, 1000), + // @ts-expect-error Combines modular and namespace API () => storage.setMaxUploadRetryTime(1000), 'setMaxUploadRetryTime', ); }); it('setMaxDownloadRetryTime()', function () { - const storage = firebase.app().storage(); + const storage = getStorage(); storageV9Deprecation( () => setMaxDownloadRetryTime(storage, 1000), + // @ts-expect-error Combines modular and namespace API () => storage.setMaxDownloadRetryTime(1000), 'setMaxDownloadRetryTime', ); }); it('delete()', function () { - const storage = firebase.app().storage(); - const storageRef = storage.ref('foo'); + const storage = getStorage(); + const storageRef = ref(storage, 'foo'); + const namespacedStorageRef = withDeprecationWarningsSilenced(() => + firebase.storage().ref('foo'), + ); storageRefV9Deprecation( () => deleteObject(storageRef), - () => storageRef.delete(), + () => namespacedStorageRef.delete(), 'delete', ); }); it('getDownloadURL()', function () { - const storage = firebase.app().storage(); - const storageRef = storage.ref('foo'); + const storage = getStorage(); + const storageRef = ref(storage, 'foo'); + const namespacedStorageRef = withDeprecationWarningsSilenced(() => + firebase.storage().ref('foo'), + ); storageRefV9Deprecation( () => getDownloadURL(storageRef), - () => storageRef.getDownloadURL(), + () => namespacedStorageRef.getDownloadURL(), 'getDownloadURL', ); }); it('getMetadata()', function () { - const storage = firebase.app().storage(); - const storageRef = storage.ref('foo'); + const storage = getStorage(); + const storageRef = ref(storage, 'foo'); + const namespacedStorageRef = withDeprecationWarningsSilenced(() => + firebase.storage().ref('foo'), + ); storageRefV9Deprecation( () => getMetadata(storageRef), - () => storageRef.getMetadata(), + () => namespacedStorageRef.getMetadata(), 'getMetadata', ); }); it('list()', function () { - const storage = firebase.app().storage(); - const storageRef = storage.ref('foo'); + const storage = getStorage(); + const storageRef = ref(storage, 'foo'); + const namespacedStorageRef = withDeprecationWarningsSilenced(() => + firebase.storage().ref('foo'), + ); storageRefV9Deprecation( () => list(storageRef), - () => storageRef.list(), + () => namespacedStorageRef.list(), 'list', ); }); it('listAll()', function () { - const storage = firebase.app().storage(); - const storageRef = storage.ref('foo'); + const storage = getStorage(); + const storageRef = ref(storage, 'foo'); + const namespacedStorageRef = withDeprecationWarningsSilenced(() => + firebase.storage().ref('foo'), + ); storageRefV9Deprecation( () => listAll(storageRef), - () => storageRef.listAll(), + () => namespacedStorageRef.listAll(), 'listAll', ); }); it('updateMetadata()', function () { - const storage = firebase.app().storage(); - const storageRef = storage.ref('foo'); + const storage = getStorage(); + const storageRef = ref(storage, 'foo'); + const namespacedStorageRef = withDeprecationWarningsSilenced(() => + firebase.storage().ref('foo'), + ); storageRefV9Deprecation( () => updateMetadata(storageRef, {}), - () => storageRef.updateMetadata({}), + () => namespacedStorageRef.updateMetadata({}), 'updateMetadata', ); }); it('put()', function () { - const storage = firebase.app().storage(); - const storageRef = storage.ref('foo'); + const storage = getStorage(); + const storageRef = ref(storage, 'foo'); + const namespacedStorageRef = withDeprecationWarningsSilenced(() => + firebase.storage().ref('foo'), + ); storageRefV9Deprecation( () => uploadBytesResumable(storageRef, new Blob(['foo']), {}), - () => storageRef.put(new Blob(['foo']), {}), + () => namespacedStorageRef.put(new Blob(['foo']), {}), 'put', ); }); it('putString()', function () { - const storage = firebase.app().storage(); - const storageRef = storage.ref('foo'); + const storage = getStorage(); + const storageRef = ref(storage, 'foo'); + const namespacedStorageRef = withDeprecationWarningsSilenced(() => + firebase.storage().ref('foo'), + ); storageRefV9Deprecation( () => uploadString(storageRef, 'foo', StringFormat.RAW), - () => storageRef.putString('foo', StringFormat.RAW), + () => namespacedStorageRef.putString('foo', StringFormat.RAW), 'putString', ); }); it('putFile()', function () { - const storage = firebase.app().storage(); - const storageRef = storage.ref('foo'); + const storage = getStorage(); + const storageRef = ref(storage, 'foo'); + const namespacedStorageRef = withDeprecationWarningsSilenced(() => + firebase.storage().ref('foo'), + ); storageRefV9Deprecation( () => putFile(storageRef, 'foo', {}), - () => storageRef.putFile('foo', {}), + () => namespacedStorageRef.putFile('foo', {}), 'putFile', ); }); it('writeToFile()', function () { - const storage = firebase.app().storage(); - const storageRef = storage.ref('foo'); + const storage = getStorage(); + const storageRef = ref(storage, 'foo'); + const namespacedStorageRef = withDeprecationWarningsSilenced(() => + firebase.storage().ref('foo'), + ); storageRefV9Deprecation( () => writeToFile(storageRef, 'foo'), - () => storageRef.writeToFile('foo'), + () => namespacedStorageRef.writeToFile('foo'), 'writeToFile', ); }); it('toString()', function () { - const storage = firebase.app().storage(); - const storageRef = storage.ref('foo'); - storageRefV9Deprecation( - () => toString(storageRef), - () => storageRef.toString(), - 'toString', - ); + const storage = getStorage(); + const storageRef = ref(storage, 'foo'); + expect(typeof storageRef.toString()).toBe('string'); + expect(storageRef.toString()).toContain('gs://'); }); - it('child()', function () { - const storage = firebase.app().storage(); - const storageRef = storage.ref('foo'); + it('ref() with child path', function () { + const storage = getStorage(); + const storageRef = ref(storage, 'foo'); + const namespacedStorageRef = withDeprecationWarningsSilenced(() => + firebase.storage().ref('foo'), + ); storageRefV9Deprecation( - () => child(storageRef, 'bar'), - () => storageRef.child('bar'), + () => ref(storageRef, 'bar'), + () => namespacedStorageRef.child('bar'), 'child', ); }); diff --git a/packages/storage/e2e/StorageReference.e2e.js b/packages/storage/e2e/StorageReference.e2e.js index 2a8a8fe01e..1813075111 100644 --- a/packages/storage/e2e/StorageReference.e2e.js +++ b/packages/storage/e2e/StorageReference.e2e.js @@ -245,10 +245,10 @@ describe('storage() -> StorageReference', function () { result.should.have.property('nextPageToken'); result.items.should.be.Array(); result.items.length.should.be.greaterThan(0); - result.items[0].constructor.name.should.eql('StorageReference'); + result.items[0].constructor.name.should.eql('Reference'); result.prefixes.should.be.Array(); result.prefixes.length.should.be.greaterThan(0); - result.prefixes[0].constructor.name.should.eql('StorageReference'); + result.prefixes[0].constructor.name.should.eql('Reference'); }); it('throws if options is not an object', function () { @@ -271,7 +271,7 @@ describe('storage() -> StorageReference', function () { result.nextPageToken.should.be.String(); result.items.should.be.Array(); result.items.length.should.eql(1); - result.items[0].constructor.name.should.eql('StorageReference'); + result.items[0].constructor.name.should.eql('Reference'); result.prefixes.should.be.Array(); // todo length? }); @@ -344,10 +344,10 @@ describe('storage() -> StorageReference', function () { should.equal(result.nextPageToken, null); result.items.should.be.Array(); result.items.length.should.be.greaterThan(0); - result.items[0].constructor.name.should.eql('StorageReference'); + result.items[0].constructor.name.should.eql('Reference'); result.prefixes.should.be.Array(); result.prefixes.length.should.be.greaterThan(0); - result.prefixes[0].constructor.name.should.eql('StorageReference'); + result.prefixes[0].constructor.name.should.eql('Reference'); }); it('should not crash if the user is not allowed to list the directory', async function () { @@ -479,15 +479,15 @@ describe('storage() -> StorageReference', function () { metadata.customMetadata.keepMe.should.equal('please'); }); - it('should error if updateMetadata includes md5hash', async function () { + it('should error if updateMetadata includes md5Hash', async function () { const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); try { await storageReference.updateMetadata({ - md5hash: '0xDEADBEEF', + md5Hash: '0xDEADBEEF', }); return Promise.reject(new Error('Did not throw on invalid updateMetadata')); } catch (e) { - e.message.should.containEql('md5hash may only be set on upload, not on updateMetadata'); + e.message.should.containEql('md5Hash may only be set on upload, not on updateMetadata'); return Promise.resolve(); } }); @@ -550,7 +550,7 @@ describe('storage() -> StorageReference', function () { return Promise.resolve(); } }); - // TODO check an metaData:md5hash property passes through correcty on putFile + // TODO check an metaData:md5Hash property passes through correcty on putFile }); describe('putString', function () { @@ -604,7 +604,7 @@ describe('storage() -> StorageReference', function () { const storageReference = firebase.storage().ref(`${PATH}/metadataTest.txt`); await storageReference.putString('foo', 'raw', { contentType: 'text/plain', - md5hash: '123412341234', + md5Hash: '123412341234', cacheControl: 'true', contentDisposition: 'disposed', contentEncoding: 'application/octet-stream', @@ -667,7 +667,7 @@ describe('storage() -> StorageReference', function () { const storageReference = firebase.storage().ref(`${PATH}/metadataTest.jpeg`); await storageReference.put(new ArrayBuffer(), { contentType: 'image/jpg', - md5hash: '123412341234', + md5Hash: '123412341234', cacheControl: 'true', contentDisposition: 'disposed', contentEncoding: 'application/octet-stream', @@ -687,7 +687,7 @@ describe('storage() -> StorageReference', function () { .ref(`${PATH}/metadataTest.jpeg`); await storageReference.put(new ArrayBuffer(), { contentType: 'image/jpg', - md5hash: '123412341234', + md5Hash: '123412341234', cacheControl: 'true', contentDisposition: 'disposed', contentEncoding: 'application/octet-stream', @@ -712,6 +712,33 @@ describe('storage() -> StorageReference', function () { secondStorage = getStorage(getApp(), secondStorageBucket); }); + describe('strict modular deprecation mode', function () { + beforeEach(function () { + // @ts-ignore + globalThis.RNFB_MODULAR_DEPRECATION_STRICT_MODE = true; + }); + + afterEach(function () { + // @ts-ignore + globalThis.RNFB_MODULAR_DEPRECATION_STRICT_MODE = false; + }); + + it('does not treat modular reference instance APIs as namespaced', function () { + const { getApp } = modular; + const { getStorage, ref } = storageModular; + const storageReference = ref(getStorage(), '/foo/uploadNope.jpeg'); + + storageReference + .toString() + .should.equal(`gs://${getApp().options.storageBucket}/foo/uploadNope.jpeg`); + storageReference.fullPath.should.equal('foo/uploadNope.jpeg'); + storageReference.bucket.should.equal(getApp().options.storageBucket); + storageReference.name.should.equal('uploadNope.jpeg'); + storageReference.parent.fullPath.should.equal('foo'); + storageReference.root.fullPath.should.equal('/'); + }); + }); + describe('second storage bucket writes to Storage emulator', function () { // Same bucket defined in app.js when setting up emulator @@ -746,12 +773,12 @@ describe('storage() -> StorageReference', function () { describe('toString()', function () { it('returns the correct bucket path to the file', function () { const { getApp } = modular; - const { getStorage, ref, toString } = storageModular; + const { getStorage, ref } = storageModular; const storageReference = ref(getStorage(), `/uploadNope.jpeg`); - toString(storageReference).should.equal( - `gs://${getApp().options.storageBucket}/uploadNope.jpeg`, - ); + storageReference + .toString() + .should.equal(`gs://${getApp().options.storageBucket}/uploadNope.jpeg`); }); }); @@ -818,10 +845,10 @@ describe('storage() -> StorageReference', function () { describe('child()', function () { it('returns a reference to a child path', function () { - const { getStorage, ref, child } = storageModular; + const { getStorage, ref } = storageModular; const storageReference = ref(getStorage(), '/foo'); - const childRef = child(storageReference, 'someFile.json'); + const childRef = ref(storageReference, 'someFile.json'); childRef.fullPath.should.equal('foo/someFile.json'); }); }); @@ -983,18 +1010,18 @@ describe('storage() -> StorageReference', function () { result.items.should.be.Array(); result.items.length.should.be.greaterThan(0); - result.items[0].constructor.name.should.eql('StorageReference'); + result.items[0].constructor.name.should.eql('Reference'); result.prefixes.should.be.Array(); result.prefixes.length.should.be.greaterThan(0); - result.prefixes[0].constructor.name.should.eql('StorageReference'); + result.prefixes[0].constructor.name.should.eql('Reference'); }); - it('throws if options is not an object', function () { + it('throws if options is not an object', async function () { try { const { getStorage, ref, list } = storageModular; const storageReference = ref(getStorage(), `${PATH}/ok.jpeg`); - list(storageReference, 123); + await list(storageReference, 123); return Promise.reject(new Error('Did not throw')); } catch (error) { error.message.should.containEql("'options' expected an object value"); @@ -1015,18 +1042,18 @@ describe('storage() -> StorageReference', function () { result.items.should.be.Array(); result.items.length.should.eql(1); - result.items[0].constructor.name.should.eql('StorageReference'); + result.items[0].constructor.name.should.eql('Reference'); result.prefixes.should.be.Array(); // todo length? }); - it('throws if maxResults is not a number', function () { + it('throws if maxResults is not a number', async function () { try { const { getStorage, ref, list } = storageModular; const storageReference = ref(getStorage(), `${PATH}/list`); - list(storageReference, { + await list(storageReference, { maxResults: '123', }); return Promise.reject(new Error('Did not throw')); @@ -1036,11 +1063,11 @@ describe('storage() -> StorageReference', function () { } }); - it('throws if maxResults is not a valid number', function () { + it('throws if maxResults is not a valid number', async function () { try { const { getStorage, ref, list } = storageModular; const storageReference = ref(getStorage(), `${PATH}/list`); - list(storageReference, { + await list(storageReference, { maxResults: 2000, }); return Promise.reject(new Error('Did not throw')); @@ -1054,11 +1081,11 @@ describe('storage() -> StorageReference', function () { }); describe('pageToken', function () { - it('throws if pageToken is not a string', function () { + it('throws if pageToken is not a string', async function () { try { const { getStorage, ref, list } = storageModular; const storageReference = ref(getStorage(), `${PATH}/list`); - list(storageReference, { + await list(storageReference, { pageToken: 123, }); return Promise.reject(new Error('Did not throw')); @@ -1101,11 +1128,11 @@ describe('storage() -> StorageReference', function () { result.items.should.be.Array(); result.items.length.should.be.greaterThan(0); - result.items[0].constructor.name.should.eql('StorageReference'); + result.items[0].constructor.name.should.eql('Reference'); result.prefixes.should.be.Array(); result.prefixes.length.should.be.greaterThan(0); - result.prefixes[0].constructor.name.should.eql('StorageReference'); + result.prefixes[0].constructor.name.should.eql('Reference'); }); it('should not crash if the user is not allowed to list the directory', async function () { @@ -1254,17 +1281,17 @@ describe('storage() -> StorageReference', function () { metadata.customMetadata.keepMe.should.equal('please'); }); - it('should error if updateMetadata includes md5hash', async function () { + it('should error if updateMetadata includes md5Hash', async function () { const { getStorage, ref, updateMetadata } = storageModular; const storageReference = ref(getStorage(), `${PATH}/list/file1.txt`); try { await updateMetadata(storageReference, { - md5hash: '0xDEADBEEF', + md5Hash: '0xDEADBEEF', }); return Promise.reject(new Error('Did not throw on invalid updateMetadata')); } catch (e) { - e.message.should.containEql('md5hash may only be set on upload, not on updateMetadata'); + e.message.should.containEql('md5Hash may only be set on upload, not on updateMetadata'); return Promise.resolve(); } }); @@ -1334,7 +1361,7 @@ describe('storage() -> StorageReference', function () { } }); - // TODO check an metaData:md5hash property passes through correcty on putFile + // TODO check an metaData:md5Hash property passes through correcty on putFile }); describe('putString', function () { @@ -1394,7 +1421,7 @@ describe('storage() -> StorageReference', function () { await uploadString(storageReference, 'foo', 'raw', { contentType: 'text/plain', - md5hash: '123412341234', + md5Hash: '123412341234', cacheControl: 'true', contentDisposition: 'disposed', contentEncoding: 'application/octet-stream', @@ -1470,7 +1497,7 @@ describe('storage() -> StorageReference', function () { await uploadBytesResumable(storageReference, new ArrayBuffer(), { contentType: 'image/jpg', - md5hash: '123412341234', + md5Hash: '123412341234', cacheControl: 'true', contentDisposition: 'disposed', contentEncoding: 'application/octet-stream', @@ -1493,7 +1520,7 @@ describe('storage() -> StorageReference', function () { await uploadBytesResumable(storageReference, new ArrayBuffer(), { contentType: 'image/jpg', - md5hash: '123412341234', + md5Hash: '123412341234', cacheControl: 'true', contentDisposition: 'disposed', contentEncoding: 'application/octet-stream', diff --git a/packages/storage/e2e/storage.e2e.js b/packages/storage/e2e/storage.e2e.js index 6305ce88e5..1a32242712 100644 --- a/packages/storage/e2e/storage.e2e.js +++ b/packages/storage/e2e/storage.e2e.js @@ -353,43 +353,47 @@ describe('storage() modular', function () { }); }); - describe('refFromURL', function () { + describe('ref with full URL (gs:// or https://)', function () { it('accepts a gs url', async function () { - const { getStorage, refFromURL, toString } = storageModular; + const { getStorage, ref } = storageModular; const url = 'gs://foo/bar/baz.png'; - const ref = refFromURL(getStorage(), url); + const refObj = ref(getStorage(), url); - toString(ref).should.equal(url); + refObj.toString().should.equal(url); }); it('accepts a https url', async function () { - const { getStorage, refFromURL } = storageModular; + const { getStorage, ref } = storageModular; const url = 'https://firebasestorage.googleapis.com/v0/b/react-native-firebase-testing.appspot.com/o/1mbTestFile.gif?alt=media'; - const ref = refFromURL(getStorage(), url); - ref.bucket.should.equal('react-native-firebase-testing.appspot.com'); - ref.name.should.equal('1mbTestFile.gif'); - ref.toString().should.equal('gs://react-native-firebase-testing.appspot.com/1mbTestFile.gif'); + const refObj = ref(getStorage(), url); + refObj.bucket.should.equal('react-native-firebase-testing.appspot.com'); + refObj.name.should.equal('1mbTestFile.gif'); + refObj + .toString() + .should.equal('gs://react-native-firebase-testing.appspot.com/1mbTestFile.gif'); }); it('accepts a https encoded url', async function () { - const { getStorage, refFromURL } = storageModular; + const { getStorage, ref } = storageModular; const url = 'https%3A%2F%2Ffirebasestorage.googleapis.com%2Fv0%2Fb%2Freact-native-firebase-testing.appspot.com%2Fo%2F1mbTestFile.gif%3Falt%3Dmedia'; - const ref = refFromURL(getStorage(), url); - ref.bucket.should.equal('react-native-firebase-testing.appspot.com'); - ref.name.should.equal('1mbTestFile.gif'); - ref.toString().should.equal('gs://react-native-firebase-testing.appspot.com/1mbTestFile.gif'); + const refObj = ref(getStorage(), url); + refObj.bucket.should.equal('react-native-firebase-testing.appspot.com'); + refObj.name.should.equal('1mbTestFile.gif'); + refObj + .toString() + .should.equal('gs://react-native-firebase-testing.appspot.com/1mbTestFile.gif'); }); it('throws an error if https url could not be parsed', async function () { try { - const { getStorage, refFromURL } = storageModular; + const { getStorage, ref } = storageModular; - refFromURL(getStorage(), 'https://invertase.io'); + ref(getStorage(), 'https://invertase.io'); return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { error.message.should.containEql("unable to parse 'url'"); @@ -398,30 +402,30 @@ describe('storage() modular', function () { }); it('accepts a gs url without a fullPath', async function () { - const { getStorage, refFromURL } = storageModular; + const { getStorage, ref } = storageModular; const url = 'gs://some-bucket'; - const ref = refFromURL(getStorage(), url); - ref.toString().should.equal(url); + const refObj = ref(getStorage(), url); + refObj.toString().should.equal(url); }); - it('throws an error if url is not a string', async function () { + it('throws a path error if argument is not a string', async function () { try { - const { getStorage, refFromURL } = storageModular; + const { getStorage, ref } = storageModular; - refFromURL(getStorage(), { derp: true }); + ref(getStorage(), { derp: true }); return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { - error.message.should.containEql("'url' must be a string value"); + error.message.should.containEql("'path' must be a string value"); return Promise.resolve(); } }); it('throws an error if url does not start with gs:// or https://', async function () { try { - const { getStorage, refFromURL } = storageModular; + const { getStorage, ref } = storageModular; - refFromURL(getStorage(), 'bs://foo/bar/cat.gif'); + ref(getStorage(), 'bs://foo/bar/cat.gif'); return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { diff --git a/packages/storage/lib/StorageDownloadTask.js b/packages/storage/lib/StorageDownloadTask.ts similarity index 81% rename from packages/storage/lib/StorageDownloadTask.js rename to packages/storage/lib/StorageDownloadTask.ts index 0d6809e524..e2fea21456 100644 --- a/packages/storage/lib/StorageDownloadTask.js +++ b/packages/storage/lib/StorageDownloadTask.ts @@ -16,11 +16,15 @@ */ import StorageTask from './StorageTask'; +import type { StorageReference, TaskSnapshot } from './types/storage'; const DOWNLOAD_TASK = 'download'; export default class StorageDownloadTask extends StorageTask { - constructor(storageRef, beginTaskFn) { + constructor( + storageRef: StorageReference, + beginTaskFn: (task: StorageTask) => Promise, + ) { super(DOWNLOAD_TASK, storageRef, beginTaskFn); } } diff --git a/packages/storage/lib/StorageListResult.js b/packages/storage/lib/StorageListResult.js deleted file mode 100644 index c0646e7657..0000000000 --- a/packages/storage/lib/StorageListResult.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library 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. - * - */ - -// To avoid React Native require cycle warnings -let StorageReference = null; -export function provideStorageReferenceClass(storageReference) { - StorageReference = storageReference; -} - -export default class StorageListResult { - constructor(storage, nativeData) { - this._nextPageToken = nativeData.nextPageToken || null; - this._items = nativeData.items.map(path => new StorageReference(storage, path)); - this._prefixes = nativeData.prefixes.map(path => new StorageReference(storage, path)); - } - - get items() { - return this._items; - } - - get nextPageToken() { - return this._nextPageToken; - } - - get prefixes() { - return this._prefixes; - } -} diff --git a/packages/storage/lib/StorageListResult.ts b/packages/storage/lib/StorageListResult.ts new file mode 100644 index 0000000000..618aeb293c --- /dev/null +++ b/packages/storage/lib/StorageListResult.ts @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 type { StorageReference } from './types/storage'; +import type { ListResultInternal, StorageInternal } from './types/internal'; + +// To avoid React Native require cycle warnings +let Reference: (new (storage: StorageInternal, path: string) => StorageReference) | null = null; + +export function provideStorageReferenceClass( + storageReference: new (storage: StorageInternal, path: string) => StorageReference, +): void { + Reference = storageReference; +} + +export default class StorageListResult { + private _nextPageToken: string | null; + private _items: StorageReference[]; + private _prefixes: StorageReference[]; + + constructor(storage: StorageInternal, nativeData: ListResultInternal) { + this._nextPageToken = nativeData.nextPageToken || null; + + if (!Reference) { + throw new Error( + 'Reference class has not been provided. This is likely a module initialization issue.', + ); + } + + // TypeScript doesn't narrow the type after the null check, so we assign to a const + const StorageReferenceClass = Reference; + this._items = nativeData.items.map(path => new StorageReferenceClass(storage, path)); + this._prefixes = nativeData.prefixes.map(path => new StorageReferenceClass(storage, path)); + } + + get items(): StorageReference[] { + return this._items; + } + + get nextPageToken(): string | null { + return this._nextPageToken; + } + + get prefixes(): StorageReference[] { + return this._prefixes; + } +} diff --git a/packages/storage/lib/StorageReference.js b/packages/storage/lib/StorageReference.ts similarity index 62% rename from packages/storage/lib/StorageReference.js rename to packages/storage/lib/StorageReference.ts index 40a61f9705..e14e10b93c 100644 --- a/packages/storage/lib/StorageReference.js +++ b/packages/storage/lib/StorageReference.ts @@ -29,15 +29,28 @@ import { pathParent, ReferenceBase, toFilePath, + isModularCall, } from '@react-native-firebase/app/dist/module/common'; import StorageDownloadTask from './StorageDownloadTask'; import StorageListResult, { provideStorageReferenceClass } from './StorageListResult'; import { StringFormat } from './StorageStatics'; import StorageUploadTask from './StorageUploadTask'; import { validateMetadata } from './utils'; - -export default class StorageReference extends ReferenceBase { - constructor(storage, path) { +import type { + StorageReference, + SettableMetadata, + UploadMetadata, + ListOptions, + FullMetadata, + Task, + FirebaseStorage, +} from './types/storage'; +import type { ListResultInternal, StorageInternal } from './types/internal'; + +export default class Reference extends ReferenceBase implements StorageReference { + _storage: StorageInternal; + + constructor(storage: StorageInternal, path: string) { super(path); this._storage = storage; } @@ -45,145 +58,162 @@ export default class StorageReference extends ReferenceBase { /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#bucket */ - get bucket() { - return this._storage._customUrlOrRegion.replace('gs://', ''); + get bucket(): string { + return this._storage._customUrlOrRegion!.replace('gs://', ''); } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#fullPath */ - get fullPath() { + get fullPath(): string { return this.path; } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#name */ - get name() { + get name(): string { return pathLastComponent(this.path); } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#parent */ - get parent() { + get parent(): StorageReference | null { const parentPath = pathParent(this.path); if (parentPath === null) { return parentPath; } - return new StorageReference(this._storage, parentPath); + return new Reference(this._storage, parentPath); } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#root */ - get root() { - return new StorageReference(this._storage, '/'); + get root(): StorageReference { + return new Reference(this._storage, '/'); } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#storage */ - get storage() { + get storage(): FirebaseStorage { return this._storage; } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#child */ - child(path) { + child(path: string): StorageReference { const childPath = pathChild(this.path, path); - return new StorageReference(this._storage, childPath); + return new Reference(this._storage, childPath); } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#delete */ - delete() { + delete(): Promise { return this._storage.native.delete(this.toString()); } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#getDownloadURL */ - getDownloadURL() { + getDownloadURL(): Promise { return this._storage.native.getDownloadURL(this.toString()); } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#getMetadata */ - getMetadata() { + getMetadata(): Promise { return this._storage.native.getMetadata(this.toString()); } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#list */ - list(options) { + list(options?: ListOptions): Promise { if (!isUndefined(options) && !isObject(options)) { throw new Error( "firebase.storage.StorageReference.list(*) 'options' expected an object value.", ); } - const listOptions = { + const listOptions: { maxResults: number; pageToken?: string } = { maxResults: 1000, }; + const isModular = isModularCall(arguments); + if (options) { if (hasOwnProperty(options, 'maxResults')) { - if (!isNumber(options.maxResults) || !isInteger(options.maxResults)) { - throw new Error( - "firebase.storage.StorageReference.list(*) 'options.maxResults' expected a number value.", - ); - } - - if (options.maxResults < 1 || options.maxResults > 1000) { - throw new Error( - "firebase.storage.StorageReference.list(*) 'options.maxResults' expected a number value between 1-1000.", - ); + const maxResults = options.maxResults; + // Modular typings allow `null` for omission - ignore safely. + if (isModular && (maxResults === null || isUndefined(maxResults))) { + // no-op (keep default) + } else { + if (!isNumber(maxResults) || !isInteger(maxResults)) { + throw new Error( + "firebase.storage.StorageReference.list(*) 'options.maxResults' expected a number value.", + ); + } + + if (maxResults < 1 || maxResults > 1000) { + throw new Error( + "firebase.storage.StorageReference.list(*) 'options.maxResults' expected a number value between 1-1000.", + ); + } + + listOptions.maxResults = maxResults; } - - listOptions.maxResults = options.maxResults; } - if (options.pageToken) { - if (!isString(options.pageToken)) { - throw new Error( - "firebase.storage.StorageReference.list(*) 'options.pageToken' expected a string value.", - ); + if (hasOwnProperty(options, 'pageToken')) { + const pageToken = options.pageToken; + // Modular typings allow `null` for omission - ignore safely. + if (isModular && (pageToken === null || isUndefined(pageToken))) { + // no-op + } else if (!isUndefined(pageToken) && pageToken !== null) { + if (!isString(pageToken)) { + throw new Error( + "firebase.storage.StorageReference.list(*) 'options.pageToken' expected a string value.", + ); + } + + listOptions.pageToken = pageToken; } - - listOptions.pageToken = options.pageToken; } } return this._storage.native .list(this.toString(), listOptions) - .then(data => new StorageListResult(this._storage, data)); + .then((data: ListResultInternal) => new StorageListResult(this._storage, data)); } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#listAll */ - listAll() { + listAll(): Promise { return this._storage.native .listAll(this.toString()) - .then(data => new StorageListResult(this._storage, data)); + .then((data: ListResultInternal) => new StorageListResult(this._storage, data)); } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#put */ - put(data, metadata) { - if (!isUndefined(metadata)) { - validateMetadata(metadata, false); - } + put(data: Blob | Uint8Array | ArrayBuffer, metadata?: UploadMetadata): Task { + const validatedMetadata = isUndefined(metadata) ? metadata : validateMetadata(metadata, false); return new StorageUploadTask(this, task => Base64.fromData(data).then(({ string, format }) => { - const { _string, _format, _metadata } = this._updateString(string, format, metadata, false); + const { _string, _format, _metadata } = this._updateString( + string as string, + format, + validatedMetadata, + false, + ); return this._storage.native.putString( this.toString(), _string, @@ -198,7 +228,11 @@ export default class StorageReference extends ReferenceBase { /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#putString */ - putString(string, format = StringFormat.RAW, metadata) { + putString( + string: string, + format: (typeof StringFormat)[keyof typeof StringFormat] = StringFormat.RAW, + metadata?: UploadMetadata, + ): Task { const { _string, _format, _metadata } = this._updateString(string, format, metadata, false); return new StorageUploadTask(this, task => @@ -209,7 +243,7 @@ export default class StorageReference extends ReferenceBase { /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#fullPath */ - toString() { + toString(): string { if (this.path.length <= 1) { return `${this._storage._customUrlOrRegion}`; } @@ -220,9 +254,9 @@ export default class StorageReference extends ReferenceBase { /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#updateMetadata */ - updateMetadata(metadata) { - validateMetadata(metadata); - return this._storage.native.updateMetadata(this.toString(), metadata); + updateMetadata(metadata: SettableMetadata): Promise { + const validatedMetadata = validateMetadata(metadata); + return this._storage.native.updateMetadata(this.toString(), validatedMetadata); } /* ---------------------------------------- @@ -232,7 +266,7 @@ export default class StorageReference extends ReferenceBase { /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference */ - writeToFile(filePath) { + writeToFile(filePath: string): Task { if (!isString(filePath)) { throw new Error( "firebase.storage.StorageReference.writeToFile(*) 'filePath' expects a string value.", @@ -247,10 +281,8 @@ export default class StorageReference extends ReferenceBase { /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Reference */ - putFile(filePath, metadata) { - if (!isUndefined(metadata)) { - validateMetadata(metadata, false); - } + putFile(filePath: string, metadata?: UploadMetadata): Task { + const validatedMetadata = isUndefined(metadata) ? metadata : validateMetadata(metadata, false); if (!isString(filePath)) { throw new Error( @@ -259,11 +291,21 @@ export default class StorageReference extends ReferenceBase { } return new StorageUploadTask(this, task => - this._storage.native.putFile(this.toString(), toFilePath(filePath), metadata, task._id), + this._storage.native.putFile( + this.toString(), + toFilePath(filePath), + validatedMetadata, + task._id, + ), ); } - _updateString(string, format, metadata, update = false) { + _updateString( + string: string, + format: (typeof StringFormat)[keyof typeof StringFormat], + metadata: UploadMetadata | undefined, + update = false, + ): { _string: string; _format: string; _metadata: UploadMetadata | undefined } { if (!isString(string)) { throw new Error( "firebase.storage.StorageReference.putString(*, _, _) 'string' expects a string value.", @@ -279,7 +321,7 @@ export default class StorageReference extends ReferenceBase { } if (!isUndefined(metadata)) { - validateMetadata(metadata, update); + metadata = validateMetadata(metadata, update) as UploadMetadata; } let _string = string; @@ -301,7 +343,7 @@ export default class StorageReference extends ReferenceBase { if (isUndefined(metadata)) { _metadata = {}; } - _metadata.contentType = mediaType; + _metadata!.contentType = mediaType; _string = base64String; _format = StringFormat.BASE64; } @@ -310,4 +352,4 @@ export default class StorageReference extends ReferenceBase { } } -provideStorageReferenceClass(StorageReference); +provideStorageReferenceClass(Reference); diff --git a/packages/storage/lib/StorageStatics.js b/packages/storage/lib/StorageStatics.ts similarity index 66% rename from packages/storage/lib/StorageStatics.js rename to packages/storage/lib/StorageStatics.ts index bd94f2d1e8..172f75a842 100644 --- a/packages/storage/lib/StorageStatics.js +++ b/packages/storage/lib/StorageStatics.ts @@ -15,19 +15,5 @@ * */ -export const StringFormat = { - RAW: 'raw', - BASE64: 'base64', - BASE64URL: 'base64url', - DATA_URL: 'data_url', -}; -export const TaskEvent = { - STATE_CHANGED: 'state_changed', -}; -export const TaskState = { - RUNNING: 'running', - PAUSED: 'paused', - SUCCESS: 'success', - CANCELLED: 'cancelled', - ERROR: 'error', -}; +// Runtime constants live in `types/storage.ts` (alongside their derived-union types). +export { StringFormat, TaskEvent, TaskState } from './types/storage'; diff --git a/packages/storage/lib/StorageTask.js b/packages/storage/lib/StorageTask.js deleted file mode 100644 index 134b662df0..0000000000 --- a/packages/storage/lib/StorageTask.js +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library 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 { isFunction, isNull, isObject } from '@react-native-firebase/app/dist/module/common'; -import { TaskEvent } from './StorageStatics'; - -let TASK_ID = 0; - -function wrapErrorEventListener(listenerFn, unsubscribe) { - return event => { - if (unsubscribe) { - setTimeout(() => unsubscribe(), 0); - } // 1 frame = 16ms, pushing to next frame - if (isFunction(listenerFn)) { - listenerFn(event.error); - } - }; -} - -function wrapSnapshotEventListener(task, listenerFn, unsubscribe) { - if (!isFunction(listenerFn)) { - return null; - } - return event => { - if (unsubscribe) { - setTimeout(() => unsubscribe(), 0); - } // 1 frame = 16ms, pushing to next frame - if (isFunction(listenerFn)) { - const snapshot = Object.assign({}, event); - snapshot.task = task; - snapshot.ref = task._ref; - - if (snapshot.metadata) { - if (!snapshot.metadata.generation) { - snapshot.metadata.generation = ''; - } - if (!snapshot.metadata.bucket) { - snapshot.metadata.bucket = task._ref.bucket; - } - if (!snapshot.metadata.metageneration) { - snapshot.metadata.metageneration = ''; - } - // // TODO(salakar): these are always here, cannot repro without, remove in 6.1.0 if no issues: - // if (!snapshot.metadata.name) snapshot.metadata.name = task._ref.name; - // if (!snapshot.metadata.fullPath) snapshot.metadata.fullPath = task._ref.fullPath; - } - - Object.freeze(snapshot); - task._snapshot = snapshot; - - listenerFn(snapshot); - } - }; -} - -function addTaskEventListener(task, eventName, listener) { - let _eventName = eventName; - if (_eventName !== TaskEvent.STATE_CHANGED) { - _eventName = `${task._type}_${eventName}`; - } - - return task._storage.emitter.addListener( - task._storage.eventNameForApp(task._id, _eventName), - listener, - ); -} - -function subscribeToEvents(task, nextOrObserver, error, complete) { - let _error; - let _errorSubscription; - - let _next; - let _nextSubscription; - - let _complete; - let _completeSubscription; - - const unsubscribe = () => { - if (_nextSubscription) { - _nextSubscription.remove(); - } - if (_errorSubscription) { - _errorSubscription.remove(); - } - if (_completeSubscription) { - _completeSubscription.remove(); - } - }; - - if (isFunction(nextOrObserver)) { - _error = wrapErrorEventListener(error, unsubscribe); - _next = wrapSnapshotEventListener(task, nextOrObserver); - _complete = wrapSnapshotEventListener(task, complete, unsubscribe); - } else if (isObject(nextOrObserver)) { - _error = wrapErrorEventListener(nextOrObserver.error, unsubscribe); - _next = wrapSnapshotEventListener(task, nextOrObserver.next); - _complete = wrapSnapshotEventListener(task, nextOrObserver.complete, unsubscribe); - } else if (isNull(nextOrObserver)) { - _error = wrapErrorEventListener(error, unsubscribe); - _complete = wrapSnapshotEventListener(task, complete, unsubscribe); - } else { - throw new Error( - "firebase.storage.StorageTask.on(*, _) 'nextOrObserver' must be a Function, an Object or Null.", - ); - } - - if (_next) { - _nextSubscription = addTaskEventListener(task, TaskEvent.STATE_CHANGED, _next); - } - - if (_error) { - _errorSubscription = addTaskEventListener(task, 'failure', _error); - } - - if (_complete) { - _completeSubscription = addTaskEventListener(task, 'success', _complete); - } - - return unsubscribe; -} - -export default class StorageTask { - constructor(type, storageRef, beginTaskFn) { - this._type = type; - this._id = TASK_ID++; - this._promise = null; - this._ref = storageRef; - this._beginTask = beginTaskFn; - this._storage = storageRef._storage; - this._snapshot = null; - } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#then - */ - get then() { - if (!this._promise) { - this._promise = this._beginTask(this); - } - - return new Promise((resolve, reject) => { - const boundPromise = this._promise.then.bind(this._promise); - - boundPromise(response => { - this._snapshot = { ...response, ref: this._ref, task: this }; - resolve(response); - }).catch(error => { - reject(error); - }); - }).then.bind(this._promise); - } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#catch - */ - get catch() { - if (!this._promise) { - this._promise = this._beginTask(this); - } - return this._promise.catch.bind(this._promise); - } - - get snapshot() { - return this._snapshot; - } - - // // NOT on Web SDK - // /** - // * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#finally - // */ - // get finally() { - // if (!this._promise) this._promise = this._beginTask(this); - // return this._promise.finally.bind(this._promise); - // } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#on - */ - on(event, nextOrObserver, error, complete) { - if (event !== TaskEvent.STATE_CHANGED) { - throw new Error( - `firebase.storage.StorageTask.on event argument must be a string with a value of '${TaskEvent.STATE_CHANGED}'`, - ); - } - - if (!this._promise) { - this._promise = this._beginTask(this); - } - - // if only event provided return the subscriber function - if (!nextOrObserver && !error && !complete) { - return subscribeToEvents.bind(null, this); - } - - return subscribeToEvents(this, nextOrObserver, error, complete); - } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#pause - */ - pause() { - return this._storage.native.setTaskStatus(this._id, 0); - } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#resume - */ - resume() { - return this._storage.native.setTaskStatus(this._id, 1); - } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#cancel - */ - cancel() { - return this._storage.native.setTaskStatus(this._id, 2); - } -} diff --git a/packages/storage/lib/StorageTask.ts b/packages/storage/lib/StorageTask.ts new file mode 100644 index 0000000000..582d96ee6e --- /dev/null +++ b/packages/storage/lib/StorageTask.ts @@ -0,0 +1,336 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 { isFunction, isNull, isObject } from '@react-native-firebase/app/dist/module/common'; +import type { EmitterSubscription } from 'react-native'; +import { TaskEvent } from './StorageStatics'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; +import type { + FullMetadata, + StorageObserver, + StorageReference, + Subscribe, + Task, + TaskSnapshot, + Unsubscribe, +} from './types/storage'; +import type { StorageReferenceInternal, StorageInternal } from './types/internal'; + +let TASK_ID = 0; + +type TaskEventType = (typeof TaskEvent)[keyof typeof TaskEvent]; + +function createEmptyMetadata(ref: StorageReference): FullMetadata { + return { + bucket: ref.bucket, + fullPath: ref.fullPath, + generation: '', + metageneration: '', + name: ref.name, + size: 0, + timeCreated: '', + updated: '', + downloadTokens: undefined, + ref, + }; +} + +function wrapErrorEventListener( + listenerFn: ((error: ReactNativeFirebase.NativeFirebaseError) => void) | null | undefined, + unsubscribe: (() => void) | null | undefined, +): (snapshot: TaskSnapshot) => void { + return (snapshot: TaskSnapshot) => { + if (unsubscribe) { + setTimeout(() => unsubscribe(), 0); + } // 1 frame = 16ms, pushing to next frame + if (isFunction(listenerFn)) { + const errorEvent = snapshot as TaskSnapshot & { + error?: ReactNativeFirebase.NativeFirebaseError; + }; + if (errorEvent.error) { + listenerFn(errorEvent.error); + } + } + }; +} + +function wrapSnapshotEventListener( + task: StorageTask, + listenerFn: ((snapshot: TaskSnapshot) => unknown) | null | undefined, + unsubscribe: (() => void) | null | undefined, +): ((snapshot: TaskSnapshot) => void) | null { + if (!isFunction(listenerFn)) { + return null; + } + return (snapshot: TaskSnapshot) => { + if (unsubscribe) { + setTimeout(() => unsubscribe(), 0); + } // 1 frame = 16ms, pushing to next frame + if (isFunction(listenerFn)) { + const taskSnapshot = Object.assign({}, snapshot); + taskSnapshot.task = task as Task; + taskSnapshot.ref = task._ref; + + if (!taskSnapshot.metadata) { + taskSnapshot.metadata = createEmptyMetadata(task._ref); + } else { + if (!taskSnapshot.metadata.generation) taskSnapshot.metadata.generation = ''; + if (!taskSnapshot.metadata.bucket) taskSnapshot.metadata.bucket = task._ref.bucket; + if (!taskSnapshot.metadata.metageneration) taskSnapshot.metadata.metageneration = ''; + if (!taskSnapshot.metadata.fullPath) taskSnapshot.metadata.fullPath = task._ref.fullPath; + if (!taskSnapshot.metadata.name) taskSnapshot.metadata.name = task._ref.name; + if (!taskSnapshot.metadata.ref) taskSnapshot.metadata.ref = task._ref; + } + + Object.freeze(taskSnapshot); + task._snapshot = taskSnapshot; + + listenerFn(taskSnapshot); + } + }; +} + +function addTaskEventListener( + task: StorageTask, + eventName: string, + listener: (snapshot: TaskSnapshot) => void, +): EmitterSubscription { + let _eventName = eventName; + if (_eventName !== TaskEvent.STATE_CHANGED) { + _eventName = `${task._type}_${eventName}`; + } + + return task._storage.emitter.addListener( + task._storage.eventNameForApp(task._id, _eventName), + listener, + ); +} + +function subscribeToEvents( + task: StorageTask, + nextOrObserver?: ((snapshot: TaskSnapshot) => unknown) | StorageObserver | null, + error?: ((error: ReactNativeFirebase.NativeFirebaseError) => unknown) | null, + complete?: (() => unknown) | null, +): () => void { + let _error: ((snapshot: TaskSnapshot) => void) | undefined; + let _errorSubscription: EmitterSubscription | undefined; + + let _next: ((snapshot: TaskSnapshot) => void) | null | undefined; + let _nextSubscription: EmitterSubscription | undefined; + + let _complete: ((snapshot: TaskSnapshot) => void) | null | undefined; + let _completeSubscription: EmitterSubscription | undefined; + + const unsubscribe = () => { + if (_nextSubscription) { + _nextSubscription.remove(); + } + if (_errorSubscription) { + _errorSubscription.remove(); + } + if (_completeSubscription) { + _completeSubscription.remove(); + } + }; + + if (isFunction(nextOrObserver)) { + _error = wrapErrorEventListener(error, unsubscribe); + _next = wrapSnapshotEventListener(task, nextOrObserver, null); + _complete = wrapSnapshotEventListener(task, complete, unsubscribe); + } else if (isObject(nextOrObserver)) { + const observer = nextOrObserver as StorageObserver; + _error = wrapErrorEventListener(observer.error, unsubscribe); + _next = wrapSnapshotEventListener(task, observer.next, null); + _complete = wrapSnapshotEventListener(task, observer.complete, unsubscribe); + } else if (isNull(nextOrObserver)) { + _error = wrapErrorEventListener(error, unsubscribe); + _complete = wrapSnapshotEventListener(task, complete, unsubscribe); + } else { + throw new Error( + "firebase.storage.StorageTask.on(*, _) 'nextOrObserver' must be a Function, an Object or Null.", + ); + } + + if (_next) { + _nextSubscription = addTaskEventListener(task, TaskEvent.STATE_CHANGED, _next); + } + + if (_error) { + _errorSubscription = addTaskEventListener(task, 'failure', _error); + } + + if (_complete) { + _completeSubscription = addTaskEventListener(task, 'success', _complete); + } + + return unsubscribe; +} + +export default class StorageTask { + _type: string; + _id: number; + _promise: Promise | null; + _ref: StorageReference; + _beginTask: (task: StorageTask) => Promise; + _storage: StorageInternal; + _snapshot: TaskSnapshot; + + constructor( + type: string, + storageRef: StorageReference, + beginTaskFn: (task: StorageTask) => Promise, + ) { + this._type = type; + this._id = TASK_ID++; + this._promise = null; + this._ref = storageRef; + this._beginTask = beginTaskFn; + this._storage = (storageRef as StorageReferenceInternal)._storage; + this._snapshot = { + bytesTransferred: 0, + totalBytes: 0, + state: 'running', + metadata: createEmptyMetadata(storageRef), + task: this as Task, + ref: storageRef, + }; + } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#then + */ + get then(): ( + onFulfilled?: ((snapshot: TaskSnapshot) => TaskSnapshot) | null, + onRejected?: ((error: ReactNativeFirebase.NativeFirebaseError) => any) | null, + ) => Promise { + if (!this._promise) { + this._promise = this._beginTask(this); + } + + const promise = this._promise; + return ( + onFulfilled?: ((snapshot: TaskSnapshot) => any) | null, + onRejected?: ((error: ReactNativeFirebase.NativeFirebaseError) => any) | null, + ) => { + return new Promise((resolve, reject) => { + promise + .then((response: any) => { + this._snapshot = { + ...response, + ref: this._ref, + task: this as Task, + state: response?.state ?? this._snapshot.state, + metadata: response?.metadata ? response.metadata : createEmptyMetadata(this._ref), + } as TaskSnapshot; + if (onFulfilled) { + resolve(onFulfilled(this._snapshot!)); + } else { + resolve(response); + } + }) + .catch((error: ReactNativeFirebase.NativeFirebaseError) => { + if (onRejected) { + resolve(onRejected(error)); + } else { + reject(error); + } + }); + }); + }; + } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#catch + */ + get catch(): ( + onRejected: (error: ReactNativeFirebase.NativeFirebaseError) => any, + ) => Promise { + if (!this._promise) { + this._promise = this._beginTask(this); + } + return this._promise!.catch.bind(this._promise); + } + + get snapshot(): TaskSnapshot { + return this._snapshot; + } + + // // NOT on Web SDK + // /** + // * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#finally + // */ + // get finally() { + // if (!this._promise) this._promise = this._beginTask(this); + // return this._promise.finally.bind(this._promise); + // } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#on + */ + on(event: TaskEventType): Subscribe; + on( + event: TaskEventType, + nextOrObserver?: StorageObserver | null | ((snapshot: TaskSnapshot) => unknown), + error?: ((error: ReactNativeFirebase.NativeFirebaseError) => unknown) | null, + complete?: (() => unknown) | null, + ): Unsubscribe; + on( + event: TaskEventType, + nextOrObserver?: StorageObserver | null | ((snapshot: TaskSnapshot) => unknown), + error?: ((error: ReactNativeFirebase.NativeFirebaseError) => unknown) | null, + complete?: (() => unknown) | null, + ): Unsubscribe | Subscribe { + if (event !== TaskEvent.STATE_CHANGED) { + throw new Error( + `firebase.storage.StorageTask.on event argument must be a string with a value of '${TaskEvent.STATE_CHANGED}'`, + ); + } + + if (!this._promise) { + this._promise = this._beginTask(this); + } + + // if only event provided return the subscriber function + if (!nextOrObserver && !error && !complete) { + return ((...args: Parameters>) => + subscribeToEvents(this, ...args)) as Subscribe; + } + + return subscribeToEvents(this, nextOrObserver, error, complete); + } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#pause + */ + pause(): Promise { + return this._storage.native.setTaskStatus(this._id, 0); + } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#pause + */ + resume(): Promise { + return this._storage.native.setTaskStatus(this._id, 1); + } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.UploadTask#cancel + */ + cancel(): Promise { + return this._storage.native.setTaskStatus(this._id, 2); + } +} diff --git a/packages/storage/lib/StorageUploadTask.js b/packages/storage/lib/StorageUploadTask.ts similarity index 81% rename from packages/storage/lib/StorageUploadTask.js rename to packages/storage/lib/StorageUploadTask.ts index b9fed92741..d27ec057fb 100644 --- a/packages/storage/lib/StorageUploadTask.js +++ b/packages/storage/lib/StorageUploadTask.ts @@ -16,11 +16,15 @@ */ import StorageTask from './StorageTask'; +import type { StorageReference, TaskSnapshot } from './types/storage'; const UPLOAD_TASK = 'upload'; export default class StorageUploadTask extends StorageTask { - constructor(storageRef, beginTaskFn) { + constructor( + storageRef: StorageReference, + beginTaskFn: (task: StorageTask) => Promise, + ) { super(UPLOAD_TASK, storageRef, beginTaskFn); } } diff --git a/packages/storage/lib/index.ts b/packages/storage/lib/index.ts new file mode 100644 index 0000000000..6bb8a01850 --- /dev/null +++ b/packages/storage/lib/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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. + * + */ + +// Export modular types from types/storage +export type * from './types/storage'; + +// Export modular API functions +export * from './modular'; + +// Export namespaced API +export type { FirebaseStorageTypes } from './types/namespaced'; +export * from './namespaced'; +export { default } from './namespaced'; diff --git a/packages/storage/lib/modular.ts b/packages/storage/lib/modular.ts new file mode 100644 index 0000000000..912ac499fe --- /dev/null +++ b/packages/storage/lib/modular.ts @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 { getApp } from '@react-native-firebase/app'; +import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; +import type { FirebaseApp } from '@react-native-firebase/app'; +import type { + FirebaseStorage, + StorageReference, + FullMetadata, + ListResult, + ListOptions, + TaskResult, + Task, + SettableMetadata, + UploadMetadata, + EmulatorMockTokenOptions, +} from './types/storage'; +import type { StorageReferenceInternal, StorageInternal } from './types/internal'; + +type WithModularDeprecationArg = F extends (...args: infer P) => infer R + ? (...args: [...P, typeof MODULAR_DEPRECATION_ARG]) => R + : never; + +function isUrl(path?: string): boolean { + if (typeof path !== 'string') { + return false; + } + + return /^[A-Za-z]+:\/\//.test(decodeURIComponent(path)); +} + +/** + * Returns a Storage instance for the given app. + * @param app - FirebaseApp. Optional. + * @param bucketUrl - Storage bucket URL. Optional. + * @returns {Storage} + */ +export function getStorage(app?: FirebaseApp, bucketUrl?: string): FirebaseStorage { + if (app) { + if (bucketUrl != null) { + return getApp(app.name).storage(bucketUrl); + } + + return getApp(app.name).storage(); + } + + if (bucketUrl != null) { + return getApp().storage(bucketUrl); + } + + return getApp().storage(); +} + +/** + * Modify this Storage instance to communicate with the Firebase Storage emulator. + * @param storage - Storage instance. + * @param host - emulator host (e.g. - 'localhost') + * @param port - emulator port (e.g. - 9199) + * @param options - `EmulatorMockTokenOptions` instance. Optional. Web only. + * @returns {void} + */ +export function connectStorageEmulator( + storage: FirebaseStorage, + host: string, + port: number, + options?: EmulatorMockTokenOptions, +): void { + return ( + (storage as StorageInternal).useEmulator as WithModularDeprecationArg< + StorageInternal['useEmulator'] + > + ).call(storage, host, port, options, MODULAR_DEPRECATION_ARG); +} + +/** + * Returns a StorageReference for the given URL or path in the default bucket. + * @param storage - FirebaseStorage instance. + * @param url - Optional gs:// or https:// URL, or path. If empty, returns root reference. + * @returns {StorageReference} + */ +export function ref(storage: FirebaseStorage, url?: string): StorageReference; +/** + * Returns a StorageReference for the given path, or the same reference if path is omitted. + * @param storageRef - StorageReference instance. + * @param path - Optional child path. If omitted, returns the same reference. + * @returns {StorageReference} + */ +export function ref(storageRef: StorageReference, path?: string): StorageReference; +export function ref( + storageOrRef: FirebaseStorage | StorageReference, + path?: string, +): StorageReference { + // ref(parentRef, path) → child reference; ref(parentRef) → same reference (firebase-js-sdk overload) + if (typeof (storageOrRef as StorageReference).fullPath === 'string') { + if (path === undefined) { + return storageOrRef as StorageReference; + } + return ( + (storageOrRef as StorageReferenceInternal).child as WithModularDeprecationArg< + StorageReferenceInternal['child'] + > + ).call(storageOrRef, path, MODULAR_DEPRECATION_ARG); + } + + const storage = storageOrRef as FirebaseStorage; + + if (path != null && isUrl(path)) { + return ( + (storage as StorageInternal).refFromURL as WithModularDeprecationArg< + StorageInternal['refFromURL'] + > + ).call(storage, path, MODULAR_DEPRECATION_ARG); + } + return ( + (storage as StorageInternal).ref as WithModularDeprecationArg + ).call(storage, path, MODULAR_DEPRECATION_ARG); +} + +/** + * Deletes the object at this reference's location. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +export function deleteObject(storageRef: StorageReference): Promise { + return ( + (storageRef as StorageReferenceInternal).delete as WithModularDeprecationArg< + StorageReferenceInternal['delete'] + > + ).call(storageRef, MODULAR_DEPRECATION_ARG); +} + +/** + * Downloads the data at the object's location. Returns an error if the object is not found. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +export function getBlob( + _storageRef: StorageReference, + _maxDownloadSizeBytes?: number, +): Promise { + throw new Error('`getBlob()` is not implemented'); +} + +/** + * Downloads the data at the object's location. Returns an error if the object is not found. + * @param storageRef - Storage `Reference` instance. + * @param maxDownloadSizeBytes - The maximum allowed size in bytes to retrieve. Web only. + * @returns {Promise} + */ +export function getBytes( + _storageRef: StorageReference, + _maxDownloadSizeBytes?: number, +): Promise { + throw new Error('`getBytes()` is not implemented'); +} + +/** + * Deletes the object at this reference's location. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +export function getDownloadURL(storageRef: StorageReference): Promise { + return ( + (storageRef as StorageReferenceInternal).getDownloadURL as WithModularDeprecationArg< + StorageReferenceInternal['getDownloadURL'] + > + ).call(storageRef, MODULAR_DEPRECATION_ARG); +} + +/** + * Fetches metadata for the object at this location, if one exists. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +export function getMetadata(storageRef: StorageReference): Promise { + return ( + (storageRef as StorageReferenceInternal).getMetadata as WithModularDeprecationArg< + StorageReferenceInternal['getMetadata'] + > + ).call(storageRef, MODULAR_DEPRECATION_ARG); +} + +/** + * Downloads the data at the object's location. This API is only available in Nodejs. + * @param storageRef - Storage `Reference` instance. + * @param maxDownloadSizeBytes - The maximum allowed size in bytes to retrieve. Web only. + * @returns {NodeJS.ReadableStream;} + */ +export function getStream( + _storageRef: StorageReference, + _maxDownloadSizeBytes?: number, +): NodeJS.ReadableStream { + throw new Error('`getStream()` is not implemented'); +} + +/** + * List items (files) and prefixes (folders) under this storage reference + * @param storageRef - Storage `Reference` instance. + * @param options - Storage `ListOptions` instance. The options list() accepts. + * @returns {Promise} + */ +export async function list( + storageRef: StorageReference, + options?: ListOptions, +): Promise { + const result = await ( + (storageRef as StorageReferenceInternal).list as WithModularDeprecationArg< + StorageReferenceInternal['list'] + > + ).call(storageRef, options, MODULAR_DEPRECATION_ARG); + + if (result.nextPageToken === null) { + delete result.nextPageToken; + } + return result; +} + +/** + * List all items (files) and prefixes (folders) under this storage reference. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +export async function listAll(storageRef: StorageReference): Promise { + const result = await ( + (storageRef as StorageReferenceInternal).listAll as WithModularDeprecationArg< + StorageReferenceInternal['listAll'] + > + ).call(storageRef, MODULAR_DEPRECATION_ARG); + + if (result.nextPageToken === null) { + delete result.nextPageToken; + } + return result; +} + +/** + * Updates the metadata for this object. + * @param storageRef - Storage `Reference` instance. + * @param metadata - A Storage `SettableMetadata` instance to update. + * @returns {Promise} + */ +export function updateMetadata( + storageRef: StorageReference, + metadata: SettableMetadata, +): Promise { + return ( + (storageRef as StorageReferenceInternal).updateMetadata as WithModularDeprecationArg< + StorageReferenceInternal['updateMetadata'] + > + ).call(storageRef, metadata, MODULAR_DEPRECATION_ARG); +} + +/** + * Uploads data to this object's location. The upload is not resumable. + * @param storageRef - Storage `Reference` instance. + * @param data - The data (Blob | Uint8Array | ArrayBuffer) to upload to the storage bucket at the reference location. + * @param metadata - A Storage `UploadMetadata` instance to update. Optional. + * @returns {Promise} + */ +export async function uploadBytes( + _storageRef: StorageReference, + _data: Blob | Uint8Array | ArrayBuffer, + _metadata?: UploadMetadata, +): Promise { + throw new Error('`uploadBytes()` is not implemented'); +} + +/** + * Uploads data to this object's location. The upload is not resumable. + * @param storageRef - Storage `Reference` instance. + * @param data - The data (Blob | Uint8Array | ArrayBuffer) to upload to the storage bucket at the reference location. + * @param metadata - A Storage `UploadMetadata` instance to update. Optional. + * @returns {Task} + */ +export function uploadBytesResumable( + storageRef: StorageReference, + data: Blob | Uint8Array | ArrayBuffer, + metadata?: UploadMetadata, +): Task { + return ( + (storageRef as StorageReferenceInternal).put as WithModularDeprecationArg< + StorageReferenceInternal['put'] + > + ).call(storageRef, data, metadata, MODULAR_DEPRECATION_ARG); +} + +/** + * Uploads data to this object's location. The upload is not resumable. + * @param storageRef - Storage `Reference` instance. + * @param value - The string to upload. + * @param format - The format of the string to upload ('raw' | 'base64' | 'base64url' | 'data_url'). Optional. + * @param metadata - A Storage `UploadMetadata` instance to update. Optional. + * @returns {Task} + */ +export function uploadString( + storageRef: StorageReference, + data: string, + format?: 'raw' | 'base64' | 'base64url' | 'data_url', + metadata?: UploadMetadata, +): Task { + return ( + (storageRef as StorageReferenceInternal).putString as WithModularDeprecationArg< + StorageReferenceInternal['putString'] + > + ).call(storageRef, data, format, metadata, MODULAR_DEPRECATION_ARG); +} + +// Methods not on the Firebase JS SDK below + +/** + * Sets the maximum time in milliseconds to retry a download if a failure occurs.. android & iOS only. + * @param storage - Storage instance. + * @param time - The new maximum operation retry time in milliseconds. + * @returns {Promise} + */ +export function setMaxOperationRetryTime(storage: FirebaseStorage, time: number): Promise { + return ( + (storage as StorageInternal).setMaxOperationRetryTime as WithModularDeprecationArg< + StorageInternal['setMaxOperationRetryTime'] + > + ).call(storage, time, MODULAR_DEPRECATION_ARG); +} + +/** + * Sets the maximum time in milliseconds to retry an upload if a failure occurs. android & iOS only. + * @param storage - Storage instance. + * @param time - The new maximum operation retry time in milliseconds. + * @returns {Promise} + */ +export function setMaxUploadRetryTime(storage: FirebaseStorage, time: number): Promise { + return ( + (storage as StorageInternal).setMaxUploadRetryTime as WithModularDeprecationArg< + StorageInternal['setMaxUploadRetryTime'] + > + ).call(storage, time, MODULAR_DEPRECATION_ARG); +} + +/** + * Puts a file from local disk onto the storage bucket. + * @param storageRef - Storage Reference instance. + * @param localFilePath The local file path to upload to the bucket at the reference location. + * @param metadata Any additional `UploadMetadata` for this task. + * @returns {Task} + */ +export function putFile( + storageRef: StorageReference, + filePath: string, + metadata?: UploadMetadata, +): Task { + return ( + (storageRef as StorageReferenceInternal).putFile as WithModularDeprecationArg< + StorageReferenceInternal['putFile'] + > + ).call(storageRef, filePath, metadata, MODULAR_DEPRECATION_ARG); +} + +/** + * Downloads a file to the specified local file path on the device. + * @param storageRef - Storage Reference instance. + * @param localFilePath The local file path to upload to on the device. + * @returns {Task} + */ +export function writeToFile(storageRef: StorageReference, filePath: string): Task { + return ( + (storageRef as StorageReferenceInternal).writeToFile as WithModularDeprecationArg< + StorageReferenceInternal['writeToFile'] + > + ).call(storageRef, filePath, MODULAR_DEPRECATION_ARG); +} + +/** + * Sets the maximum time in milliseconds to retry a download if a failure occurs. + * @param storage - Storage instance. + * @param time - The new maximum download retry time in milliseconds. + * @returns {Promise} + */ +export function setMaxDownloadRetryTime(storage: FirebaseStorage, time: number): Promise { + return ( + (storage as StorageInternal).setMaxDownloadRetryTime as WithModularDeprecationArg< + StorageInternal['setMaxDownloadRetryTime'] + > + ).call(storage, time, MODULAR_DEPRECATION_ARG); +} + +export { StringFormat, TaskEvent, TaskState } from './StorageStatics'; diff --git a/packages/storage/lib/modular/index.d.ts b/packages/storage/lib/modular/index.d.ts deleted file mode 100644 index 0c3a74b87b..0000000000 --- a/packages/storage/lib/modular/index.d.ts +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library 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 { ReactNativeFirebase } from '@react-native-firebase/app'; -import { FirebaseStorageTypes } from '../index'; - -import Storage = FirebaseStorageTypes.Module; -import Reference = FirebaseStorageTypes.Reference; -import FullMetadata = FirebaseStorageTypes.FullMetadata; -import ListResult = FirebaseStorageTypes.ListResult; -import TaskResult = FirebaseStorageTypes.TaskResult; -import Task = FirebaseStorageTypes.Task; -import ListOptions = FirebaseStorageTypes.ListOptions; -import SettableMetadata = FirebaseStorageTypes.SettableMetadata; -import EmulatorMockTokenOptions = FirebaseStorageTypes.EmulatorMockTokenOptions; -import FirebaseApp = ReactNativeFirebase.FirebaseApp; - -export const StringFormat: FirebaseStorageTypes.StringFormat; -export const TaskEvent: FirebaseStorageTypes.TaskEvent; -export const TaskState: FirebaseStorageTypes.TaskState; - -/** - * Union of literal string values in StringFormat "enum" object - */ -export type StringFormat = (typeof StringFormat)[keyof typeof StringFormat]; - -/** - * Returns the existing default {@link Storage} instance that is associated with the - * default {@link FirebaseApp}. The default storage bucket is used. If no instance exists, initializes a new - * instance with default settings. - * @param app - Firebase app to get FirebaseStorage instance for. - * @param bucketUrl - The gs:// url to your Firebase Storage Bucket. If not passed, uses the app's default Storage Bucket. - * @returns The {@link Storage} instance of the provided app. - */ -export declare function getStorage(app?: FirebaseApp, bucketUrl?: string): Storage; - -/** - * Returns the existing default {@link Storage} instance that is associated with the - * provided {@link FirebaseApp}. The default storage bucket is used. If no instance exists, initializes a new - * instance with default settings. - * - * @param app - The {@link FirebaseApp} instance that the returned {@link Storage} - * instance is associated with. - * @returns The {@link Storage} instance of the provided app. - */ -export declare function getStorage(app?: FirebaseApp): Storage; - -/** - * Returns the existing default {@link Storage} instance that is associated with the - * provided {@link FirebaseApp}. If no instance exists, initializes a new - * instance with default settings. - * - * @param app - The {@link FirebaseApp} instance that the returned {@link Storage} - * instance is associated with. If `null` the default app is used. - * @param bucketUrl - The gs:// url to the Firebase Storage Bucket. If `null` the default bucket is used. - * @returns The {@link Storage} instance of the provided app. - */ -export declare function getStorage(app?: FirebaseApp, bucketUrl?: string): Storage; - -export function getStorage(app?: FirebaseApp, bucketUrl?: string): Storage; - -/** - * Connects a {@link Storage} instance to the Firebase Storage emulator. - * @param storage - A reference to the `Storage` instance. - * @param host - Emulator host, e.g., 'localhost'. - * @param port - Emulator port, e.g., 9199. - * @param options - Optional. {@link EmulatorMockTokenOptions} instance. - */ -export function connectStorageEmulator( - storage: Storage, - host: string, - port: number, - options?: EmulatorMockTokenOptions, -): void; - -/** - * Creates a {@link Reference} from a given path or URL. - * @param storage - The {@link Storage} instance. - * @param path - Optional. A string pointing to a location within the storage bucket. - * @returns A new {@link Reference}. - */ -export function ref(storage: Storage, path?: string): Reference; - -/** - * Deletes the object at the given reference's location. - * @param storageRef - The {@link Reference} to the object to delete. - * @returns A promise that resolves when the delete is complete. - */ -export function deleteObject(storageRef: Reference): Promise; - -/** - * Retrieves the blob at the given reference's location. Throws an error if the object is not found. - * @param storageRef - The {@link Reference} to the object. - * @param maxDownloadSizeBytes - Optional. Maximum size in bytes to retrieve. - * @returns A promise resolving to the Blob. - */ -export function getBlob(storageRef: Reference, maxDownloadSizeBytes?: number): Promise; - -/** - * Retrieves bytes (up to the specified max size) from an object at the given reference's location. - * Throws an error if the object is not found or if the size exceeds the maximum allowed. - * @param storageRef - The {@link Reference} to the object. - * @param maxDownloadSizeBytes - Optional. Maximum size in bytes to retrieve. - * @returns A promise resolving to an ArrayBuffer. - */ -export function getBytes( - storageRef: Reference, - maxDownloadSizeBytes?: number, -): Promise; - -/** - * Retrieves a long-lived download URL for the object at the given reference's location. - * @param storageRef - The {@link Reference} to the object. - * @returns A promise resolving to the URL string. - */ -export function getDownloadURL(storageRef: Reference): Promise; - -/** - * Retrieves metadata for the object at the given reference's location. - * @param storageRef - The {@link Reference} to the object. - * @returns A promise resolving to the object's {@link FullMetadata}. - */ -export function getMetadata(storageRef: Reference): Promise; - -/** - * Retrieves a readable stream for the object at the given reference's location. This API is only available in Node.js. - * @param storageRef - The {@link Reference} to the object. - * @param maxDownloadSizeBytes - Optional. Maximum size in bytes to retrieve. - * @returns A NodeJS ReadableStream. - */ -export function getStream( - storageRef: Reference, - maxDownloadSizeBytes?: number, -): NodeJS.ReadableStream; - -/** - * Lists items and prefixes under the given reference. - * @param storageRef - The {@link Reference} under which to list items. - * @param options - Optional. Configuration for listing. - * @returns A promise resolving to a {@link ListResult}. - */ -export function list(storageRef: Reference, options?: ListOptions): Promise; - -/** - * Lists all items and prefixes under the given reference. - * @param storageRef - The {@link Reference} under which to list items. - * @returns A promise resolving to a {@link ListResult}. - */ -export function listAll(storageRef: Reference): Promise; - -/** - * Updates metadata for the object at the given reference. - * @param storageRef - The {@link Reference} to the object. - * @param metadata - The metadata to update. - * @returns A promise resolving to the updated {@link FullMetadata}. - */ -export function updateMetadata( - storageRef: Reference, - metadata: SettableMetadata, -): Promise; - -/** - * Uploads data to the object's location at the given reference. The upload is not resumable. - * @param storageRef - The {@link Reference} where the data should be uploaded. - * @param data - The data to upload. - * @param metadata - Optional. Metadata to associate with the uploaded object. - * @returns A promise resolving to a {@link TaskResult}. - */ -export function uploadBytes( - storageRef: Reference, - data: Blob | Uint8Array | ArrayBuffer, - metadata?: SettableMetadata, -): Promise; - -/** - * Initiates a resumable upload session for the data to the object's location at the given reference. - * @param storageRef - The {@link Reference} where the data should be uploaded. - * @param data - The data to upload. - * @param metadata - Optional. Metadata to associate with the uploaded object. - * @returns A {@link Task} associated with the upload process. - */ -export function uploadBytesResumable( - storageRef: Reference, - data: Blob | Uint8Array | ArrayBuffer, - metadata?: SettableMetadata, -): Task; - -/** - * Uploads a string to the object's location at the given reference. The string format must be specified. - * @param storageRef - The {@link Reference} where the string should be uploaded. - * @param data - The string data to upload. - * @param format - Optional. The format of the string ('raw', 'base64', 'base64url', 'data_url'). - * @param metadata - Optional. Metadata to associate with the uploaded object. - * @returns A {@link Task} associated with the upload process. - */ -export function uploadString( - storageRef: Reference, - data: string, - format?: StringFormat, - metadata?: SettableMetadata, -): Task; - -/** - * Creates a {@link Reference} from a storage bucket URL. - * @param storage - The {@link Storage} instance. - * @param url - A URL pointing to a file or location in a storage bucket. - * @returns A {@link Reference} pointing to the specified URL. - */ -export function refFromURL(storage: Storage, url: string): Reference; - -/** - * Sets the maximum time in milliseconds to retry operations other than upload and download if a failure occurs. - * @param storage - The {@link Storage} instance. - * @param time - The new maximum operation retry time in milliseconds. - */ -export function setMaxOperationRetryTime(storage: Storage, time: number): Promise; - -/** - * Sets the maximum time in milliseconds to retry upload operations if a failure occurs. - * @param storage - The {@link Storage} instance. - * @param time - The new maximum upload retry time in milliseconds. - */ -export function setMaxUploadRetryTime(storage: Storage, time: number): Promise; - -/** - * Puts a file from a local disk onto the storage bucket at the given reference. - * @param storageRef - The {@link Reference} where the file should be uploaded. - * @param filePath - The local file path of the file to upload. - * @param metadata - Optional. Metadata to associate with the uploaded file. - * @returns A {@link Task} associated with the upload process. - */ -export function putFile(storageRef: Reference, filePath: string, metadata?: SettableMetadata): Task; - -/** - * Downloads a file to the specified local file path on the device. - * @param storageRef - The {@link Reference} from which the file should be downloaded. - * @param filePath - The local file path where the file should be written. - * @returns A {@link Task} associated with the download process. - */ -export function writeToFile(storageRef: Reference, filePath: string): Task; - -/** - * Returns a gs:// URL for the object at the given reference. - * @param storageRef - The {@link Reference} to the object. - * @returns The URL as a string. - */ -export function toString(storageRef: Reference): string; - -/** - * Returns a reference to a relative path from the given reference. - * @param storageRef - The {@link Reference} as the base. - * @param path - The relative path from the base reference. - * @returns A new {@link Reference}. - */ -export function child(storageRef: Reference, path: string): Reference; - -/** - * Sets the maximum time in milliseconds to retry download operations if a failure occurs. - * @param storage - The {@link Storage} instance. - * @param time - The new maximum download retry time in milliseconds. - */ -export function setMaxDownloadRetryTime(storage: Storage, time: number): Promise; diff --git a/packages/storage/lib/modular/index.js b/packages/storage/lib/modular/index.js deleted file mode 100644 index 9e47536201..0000000000 --- a/packages/storage/lib/modular/index.js +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library 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. - * - */ -/** - * @typedef {import('..').FirebaseStorageTypes} FirebaseStorageTypes - * @typedef {import('..').FirebaseStorageTypes.Module} Storage - * @typedef {import('..').FirebaseStorageTypes.Reference} Reference - * @typedef {import('..').FirebaseStorageTypes.FullMetadata} FullMetadata - * @typedef {import('..').FirebaseStorageTypes.ListResult} ListResult - * @typedef {import('..').FirebaseStorageTypes.TaskResult} TaskResult - * @typedef {import('..').FirebaseStorageTypes.Task} Task - * @typedef {import('..').FirebaseStorageTypes.ListOptions} ListOptions - * @typedef {import('..').FirebaseStorageTypes.SettableMetadata} SettableMetadata - * @typedef {import('..').FirebaseStorageTypes.EmulatorMockTokenOptions} EmulatorMockTokenOptions - * @typedef {import('@firebase/app').FirebaseApp} FirebaseApp - */ - -import { getApp } from '@react-native-firebase/app'; -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; - -/** - * Returns a Storage instance for the given app. - * @param app - FirebaseApp. Optional. - * @param bucketUrl - Storage bucket URL. Optional. - * @returns {Storage} - */ -export function getStorage(app, bucketUrl) { - if (app) { - if (bucketUrl != null) { - return getApp(app.name).storage(bucketUrl); - } - - return getApp(app.name).storage(); - } - - if (bucketUrl != null) { - return getApp().storage(bucketUrl); - } - - return getApp().storage(); -} - -/** - * Modify this Storage instance to communicate with the Firebase Storage emulator. - * @param storage - Storage instance. - * @param host - emulator host (e.g. - 'localhost') - * @param port - emulator port (e.g. - 9199) - * @param options - `EmulatorMockTokenOptions` instance. Optional. Web only. - * @returns {void} - */ -export function connectStorageEmulator(storage, host, port, options) { - return storage.useEmulator.call(storage, host, port, options, MODULAR_DEPRECATION_ARG); -} - -/** - * Modify this Storage instance to communicate with the Firebase Storage emulator. - * @param storage - Storage instance. - * @param path An optional string pointing to a location on the storage bucket. If no path - * is provided, the returned reference will be the bucket root path. Optional. - * @returns {Reference} - */ -export function ref(storage, path) { - return storage.ref.call(storage, path, MODULAR_DEPRECATION_ARG); -} - -/** - * Deletes the object at this reference's location. - * @param storageRef - Storage `Reference` instance. - * @returns {Promise} - */ -export function deleteObject(storageRef) { - return storageRef.delete.call(storageRef, MODULAR_DEPRECATION_ARG); -} - -/** - * Downloads the data at the object's location. Returns an error if the object is not found. - * @param storageRef - Storage `Reference` instance. - * @returns {Promise} - */ -// eslint-disable-next-line -export function getBlob(storageRef, maxDownloadSizeBytes) { - throw new Error('`getBlob()` is not implemented'); -} - -/** - * Downloads the data at the object's location. Returns an error if the object is not found. - * @param storageRef - Storage `Reference` instance. - * @param maxDownloadSizeBytes - The maximum allowed size in bytes to retrieve. Web only. - * @returns {Promise} - */ -// eslint-disable-next-line -export function getBytes(storageRef, maxDownloadSizeBytes) { - throw new Error('`getBytes()` is not implemented'); -} - -/** - * Deletes the object at this reference's location. - * @param storageRef - Storage `Reference` instance. - * @returns {Promise} - */ -export function getDownloadURL(storageRef) { - return storageRef.getDownloadURL.call(storageRef, MODULAR_DEPRECATION_ARG); -} - -/** - * Fetches metadata for the object at this location, if one exists. - * @param storageRef - Storage `Reference` instance. - * @returns {Promise} - */ -export function getMetadata(storageRef) { - return storageRef.getMetadata.call(storageRef, MODULAR_DEPRECATION_ARG); -} - -/** - * Downloads the data at the object's location. This API is only available in Nodejs. - * @param storageRef - Storage `Reference` instance. - * @param maxDownloadSizeBytes - The maximum allowed size in bytes to retrieve. Web only. - * @returns {NodeJS.ReadableStream;} - */ -// eslint-disable-next-line -export function getStream(storageRef, maxDownloadSizeBytes) { - throw new Error('`getStream()` is not implemented'); -} - -/** - * List items (files) and prefixes (folders) under this storage reference - * @param storageRef - Storage `Reference` instance. - * @param options - Storage `ListOptions` instance. The options list() accepts. - * @returns {Promise} - */ -export function list(storageRef, options) { - return storageRef.list.call(storageRef, options, MODULAR_DEPRECATION_ARG); -} - -/** - * List all items (files) and prefixes (folders) under this storage reference. - * @param storageRef - Storage `Reference` instance. - * @returns {Promise} - */ -export function listAll(storageRef) { - return storageRef.listAll.call(storageRef, MODULAR_DEPRECATION_ARG); -} - -/** - * Updates the metadata for this object. - * @param storageRef - Storage `Reference` instance. - * @param metadata - A Storage `SettableMetadata` instance to update. - * @returns {Promise} - */ -export function updateMetadata(storageRef, metadata) { - return storageRef.updateMetadata.call(storageRef, metadata, MODULAR_DEPRECATION_ARG); -} - -/** - * Uploads data to this object's location. The upload is not resumable. - * @param storageRef - Storage `Reference` instance. - * @param data - The data (Blob | Uint8Array | ArrayBuffer) to upload to the storage bucket at the reference location. - * @param metadata - A Storage `SettableMetadata` instance to update. Optional. - * @returns {Promise} - */ -// eslint-disable-next-line -export async function uploadBytes(storageRef, data, metadata) { - throw new Error('`uploadBytes()` is not implemented'); -} - -/** - * Uploads data to this object's location. The upload is not resumable. - * @param storageRef - Storage `Reference` instance. - * @param data - The data (Blob | Uint8Array | ArrayBuffer) to upload to the storage bucket at the reference location. - * @param metadata - A Storage `SettableMetadata` instance to update. Optional. - * @returns {Task} - */ -export function uploadBytesResumable(storageRef, data, metadata) { - return storageRef.put.call(storageRef, data, metadata, MODULAR_DEPRECATION_ARG); -} - -/** - * Uploads data to this object's location. The upload is not resumable. - * @param storageRef - Storage `Reference` instance. - * @param value - The string to upload. - * @param format - The format of the string to upload ('raw' | 'base64' | 'base64url' | 'data_url'). Optional. - * @param metadata - A Storage `SettableMetadata` instance to update. Optional. - * @returns {Task} - */ -export function uploadString(storageRef, data, format, metadata) { - return storageRef.putString.call(storageRef, data, format, metadata, MODULAR_DEPRECATION_ARG); -} - -// Methods not on the Firebase JS SDK below - -/** - * Returns a new Storage `Reference` instance from a storage bucket URL. - * @param storage - Storage instance. - * @param url - A storage bucket URL pointing to a single file or location. Must be either a `gs://` url or an `http` url. Not available on web. - * @returns {Reference} - */ -export function refFromURL(storage, url) { - return storage.refFromURL.call(storage, url, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets the maximum time in milliseconds to retry a download if a failure occurs.. android & iOS only. - * @param storage - Storage instance. - * @param time - The new maximum operation retry time in milliseconds. - * @returns {Promise} - */ -export function setMaxOperationRetryTime(storage, time) { - return storage.setMaxOperationRetryTime.call(storage, time, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets the maximum time in milliseconds to retry an upload if a failure occurs. android & iOS only. - * @param storage - Storage instance. - * @param time - The new maximum operation retry time in milliseconds. - * @returns {Promise} - */ -export function setMaxUploadRetryTime(storage, time) { - return storage.setMaxUploadRetryTime.call(storage, time, MODULAR_DEPRECATION_ARG); -} - -/** - * Puts a file from local disk onto the storage bucket. - * @param storageRef - Storage Reference instance. - * @param localFilePath The local file path to upload to the bucket at the reference location. - * @param metadata Any additional `SettableMetadata` for this task. - * @returns {Task} - */ -export function putFile(storageRef, filePath, metadata) { - return storageRef.putFile.call(storageRef, filePath, metadata, MODULAR_DEPRECATION_ARG); -} - -/** - * Downloads a file to the specified local file path on the device. - * @param storageRef - Storage Reference instance. - * @param localFilePath The local file path to upload to on the device. - * @returns {Task} - */ -export function writeToFile(storageRef, filePath) { - return storageRef.writeToFile.call(storageRef, filePath, MODULAR_DEPRECATION_ARG); -} - -/** - * Returns a gs:// URL for this object in the form `gs://///`. - * @param storageRef - Storage Reference instance. - * @returns {String} - */ -export function toString(storageRef) { - return storageRef.toString.call(storageRef, MODULAR_DEPRECATION_ARG); -} - -/** - * Returns a reference to a relative path from this reference. - * @param storageRef - Storage Reference instance. - * @param path - The relative path from this reference. Leading, trailing, and consecutive slashes are removed. - * @returns {String} - */ -export function child(storageRef, path) { - return storageRef.child.call(storageRef, path, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets the maximum time in milliseconds to retry a download if a failure occurs. - * @param storage - Storage instance. - * @param time - The new maximum download retry time in milliseconds. - * @returns {Promise} - */ -export function setMaxDownloadRetryTime(storage, time) { - return storage.setMaxDownloadRetryTime.call(storage, time, MODULAR_DEPRECATION_ARG); -} - -export { StringFormat, TaskEvent, TaskState } from '../StorageStatics'; diff --git a/packages/storage/lib/index.js b/packages/storage/lib/namespaced.ts similarity index 73% rename from packages/storage/lib/index.js rename to packages/storage/lib/namespaced.ts index a43c61bed9..9ed9c54d8c 100644 --- a/packages/storage/lib/index.js +++ b/packages/storage/lib/namespaced.ts @@ -27,14 +27,17 @@ import { createModuleNamespace, FirebaseModule, getFirebaseRoot, + type ModuleConfig, } from '@react-native-firebase/app/dist/module/internal'; -import StorageReference from './StorageReference'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; +import Reference from './StorageReference'; import { StringFormat, TaskEvent, TaskState } from './StorageStatics'; import { getGsUrlParts, getHttpUrlParts, handleStorageEvent } from './utils'; -import version from './version'; +import { version } from './version'; import fallBackModule from './web/RNFBStorageModule'; +import type { StorageInternal } from './types/internal'; +import type { FirebaseStorageTypes } from './types/namespaced'; -// import { STATICS } from '@react-native-firebase/storage'; const statics = { StringFormat, TaskEvent, @@ -45,10 +48,20 @@ const namespace = 'storage'; const nativeEvents = ['storage_event']; const nativeModuleName = 'RNFBStorageModule'; -class FirebaseStorageModule extends FirebaseModule { - constructor(app, config, bucketUrl) { - super(app, config, bucketUrl); - if (bucketUrl === undefined) { +class FirebaseStorageModule extends FirebaseModule { + emulatorHost: string | undefined; + emulatorPort: number; + _maxUploadRetryTime: number; + _maxDownloadRetryTime: number; + _maxOperationRetryTime: number; + + constructor( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + bucketUrl?: string | null, + ) { + super(app, config, bucketUrl ?? undefined); + if (bucketUrl == null) { this._customUrlOrRegion = `gs://${app.options.storageBucket}`; } else if (!isString(bucketUrl) || !bucketUrl.startsWith('gs://')) { throw new Error( @@ -57,7 +70,7 @@ class FirebaseStorageModule extends FirebaseModule { } this.emitter.addListener( - this.eventNameForApp(nativeEvents[0]), + this.eventNameForApp(nativeEvents[0]!), handleStorageEvent.bind(null, this), ); @@ -72,46 +85,46 @@ class FirebaseStorageModule extends FirebaseModule { /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setmaxuploadretrytime */ - get maxUploadRetryTime() { + get maxUploadRetryTime(): number { return this._maxUploadRetryTime; } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setmaxdownloadretrytime */ - get maxDownloadRetryTime() { + get maxDownloadRetryTime(): number { return this._maxDownloadRetryTime; } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#maxoperationretrytime */ - get maxOperationRetryTime() { + get maxOperationRetryTime(): number { return this._maxOperationRetryTime; } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#ref */ - ref(path = '/') { + ref(path: string = '/'): Reference { if (!isString(path)) { throw new Error("firebase.storage().ref(*) 'path' must be a string value."); } - return createDeprecationProxy(new StorageReference(this, path)); + return createDeprecationProxy(new Reference(this, path)) as Reference; } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#refFromURL */ - refFromURL(url) { + refFromURL(url: string): Reference { if (!isString(url) || (!url.startsWith('gs://') && !url.startsWith('http'))) { throw new Error( "firebase.storage().refFromURL(*) 'url' must be a string value and begin with 'gs://' or 'https://'.", ); } - let path; - let bucket; + let path: string; + let bucket: string; if (url.startsWith('http')) { const parts = getHttpUrlParts(url); @@ -126,13 +139,13 @@ class FirebaseStorageModule extends FirebaseModule { } const storageInstance = this.app.storage(bucket); - return new StorageReference(storageInstance, path); + return new Reference(storageInstance as unknown as StorageInternal, path); } /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setMaxOperationRetryTime */ - setMaxOperationRetryTime(time) { + setMaxOperationRetryTime(time: number): Promise { if (!isNumber(time)) { throw new Error( "firebase.storage().setMaxOperationRetryTime(*) 'time' must be a number value.", @@ -146,7 +159,7 @@ class FirebaseStorageModule extends FirebaseModule { /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setMaxUploadRetryTime */ - setMaxUploadRetryTime(time) { + setMaxUploadRetryTime(time: number): Promise { if (!isNumber(time)) { throw new Error("firebase.storage().setMaxUploadRetryTime(*) 'time' must be a number value."); } @@ -158,7 +171,7 @@ class FirebaseStorageModule extends FirebaseModule { /** * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setMaxDownloadRetryTime */ - setMaxDownloadRetryTime(time) { + setMaxDownloadRetryTime(time: number): Promise { if (!isNumber(time)) { throw new Error( "firebase.storage().setMaxDownloadRetryTime(*) 'time' must be a number value.", @@ -169,7 +182,11 @@ class FirebaseStorageModule extends FirebaseModule { return this.native.setMaxDownloadRetryTime(time); } - useEmulator(host, port) { + useEmulator( + host: string, + port: number, + _options?: FirebaseStorageTypes.EmulatorMockTokenOptions, + ): void { if (!host || !isString(host) || !port || !isNumber(port)) { throw new Error('firebase.storage().useEmulator() takes a non-empty host and port'); } @@ -191,16 +208,15 @@ class FirebaseStorageModule extends FirebaseModule { this.emulatorHost = host; this.emulatorPort = port; this.native.useEmulator(_host, port, this._customUrlOrRegion); - return [_host, port]; // undocumented return, just used to unit test android host remapping + // @ts-ignore undocumented return, just used to unit test android host remapping + return [_host, port]; } } // import { SDK_VERSION } from '@react-native-firebase/storage'; export const SDK_VERSION = version; -// import storage from '@react-native-firebase/storage'; -// storage().X(...); -export default createModuleNamespace({ +const storageNamespace = createModuleNamespace({ statics, version, namespace, @@ -212,11 +228,31 @@ export default createModuleNamespace({ ModuleClass: FirebaseStorageModule, }); +type StorageNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< + FirebaseStorageTypes.Module, + FirebaseStorageTypes.Statics +> & { + storage: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< + FirebaseStorageTypes.Module, + FirebaseStorageTypes.Statics + >; + firebase: ReactNativeFirebase.Module; + app(name?: string): ReactNativeFirebase.FirebaseApp; +}; + +// import storage from '@react-native-firebase/storage'; +// storage().X(...); +export default storageNamespace as unknown as StorageNamespace; + // import storage, { firebase } from '@react-native-firebase/storage'; // storage().X(...); // firebase.storage().X(...); -export const firebase = getFirebaseRoot(); - -export * from './modular'; +export const firebase = + getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< + 'storage', + FirebaseStorageTypes.Module, + FirebaseStorageTypes.Statics, + true + >; setReactNativeModule(nativeModuleName, fallBackModule); diff --git a/packages/storage/lib/types/internal.ts b/packages/storage/lib/types/internal.ts new file mode 100644 index 0000000000..196ab98e1b --- /dev/null +++ b/packages/storage/lib/types/internal.ts @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 type { ModuleConfig } from '@react-native-firebase/app/dist/module/types/internal'; +import type { + FirebaseStorage, + StorageReference, + EmulatorMockTokenOptions, + SettableMetadata, + UploadMetadata, + Task, + TaskSnapshot, + FullMetadata, + ListResult, + ListOptions, +} from './storage'; +import type EventEmitter from 'react-native/Libraries/vendor/emitter/EventEmitter'; + +/** + * Internal Storage type with access to private properties. + * Used internally by StorageReference and other internal classes. + */ +export type StorageInternal = FirebaseStorage & { + native: RNFBStorageModule; + _customUrlOrRegion: string | null; + emitter: EventEmitter; + eventNameForApp: (...args: Array) => string; + _config: ModuleConfig; + /** + * Returns a reference to the object at the specified path. + * + * @param path - An optional string pointing to a location on the storage bucket. If no path + * is provided, the returned reference will be the bucket root path. + */ + ref(path?: string): StorageReference; + + /** + * Returns a reference to the object at the specified URL. + * + * @param url - A storage bucket URL pointing to a single file or location. Must be either a `gs://` url or an `http` url. + */ + refFromURL(url: string): StorageReference; + + /** + * Sets the maximum time in milliseconds to retry operations if a failure occurs. + * + * @param time - The new maximum operation retry time in milliseconds. + */ + setMaxOperationRetryTime(time: number): Promise; + + /** + * Sets the maximum time in milliseconds to retry an upload if a failure occurs. + * + * @param time - The new maximum upload retry time in milliseconds. + */ + setMaxUploadRetryTime(time: number): Promise; + + /** + * Sets the maximum time in milliseconds to retry a download if a failure occurs. + * + * @param time - The new maximum download retry time in milliseconds. + */ + setMaxDownloadRetryTime(time: number): Promise; + + /** + * Changes this instance to point to a Cloud Storage emulator running locally. + * + * @param host - The host of the emulator, e.g. "localhost" or "10.0.2.2" for Android. + * @param port - The port of the emulator, e.g. 9199. + * @param options - Optional settings for the emulator connection (web only). + * @returns {void} - Undocumented return, just used to unit test android host remapping. + */ + useEmulator(host: string, port: number, options?: EmulatorMockTokenOptions): void; +}; + +/** + * Internal Reference type with access to private properties. + * Used internally by StorageTask and other internal classes. + */ +export type StorageReferenceInternal = StorageReference & { + _storage: StorageInternal; + child(path: string): StorageReference; + delete(): Promise; + getDownloadURL(): Promise; + getMetadata(): Promise; + list(options?: ListOptions): Promise; + listAll(): Promise; + put(data: Blob | Uint8Array | ArrayBuffer, metadata?: UploadMetadata): Task; + putString( + string: string, + format?: 'raw' | 'base64' | 'base64url' | 'data_url', + metadata?: UploadMetadata, + ): Task; + updateMetadata(metadata: SettableMetadata): Promise; + writeToFile(filePath: string): Task; + putFile(filePath: string, metadata?: UploadMetadata): Task; +}; + +/** + * Internal ListResult type with access to private properties. + * Used internally by StorageListResult and other internal classes. + */ +export type ListResultInternal = { + nextPageToken?: string | null; + items: string[]; + prefixes: string[]; +}; + +/** + * Wrapped native module interface for Storage. + * + * Note: React Native Firebase internally wraps native methods and auto-prepends the app name + * when `hasMultiAppSupport` is enabled. This interface represents the *wrapped* module shape + * that is exposed as `this.native` within FirebaseModule subclasses. + */ +export interface RNFBStorageModule { + /** + * Native constants exposed on the module. + * These are read during module construction to seed the JS-side values. + */ + maxUploadRetryTime?: number; + maxDownloadRetryTime?: number; + maxOperationRetryTime?: number; + + setMaxOperationRetryTime(time: number): Promise; + setMaxUploadRetryTime(time: number): Promise; + setMaxDownloadRetryTime(time: number): Promise; + + delete(url: string): Promise; + getDownloadURL(url: string): Promise; + getMetadata(url: string): Promise; + updateMetadata(url: string, metadata: SettableMetadata): Promise; + + list( + url: string, + options: { maxResults: number; pageToken?: string }, + ): Promise; + listAll(url: string): Promise; + + putString( + url: string, + data: string, + format: string, + metadata: UploadMetadata | undefined, + taskId: number, + ): Promise; + putFile( + url: string, + filePath: string, + metadata: UploadMetadata | undefined, + taskId: number, + ): Promise; + writeToFile(url: string, filePath: string, taskId: number): Promise; + + /** + * Set a running task status. + * - 0: pause + * - 1: resume + * - 2: cancel + */ + setTaskStatus(taskId: number, status: number): Promise; + + /** + * Configure storage emulator. + * + * iOS does not persist emulator state between calls, so JS sends bucket/customUrlOrRegion through. + */ + useEmulator(host: string, port: number, customUrlOrRegion?: string | null): void; +} + +declare module '@react-native-firebase/app/dist/module/internal/NativeModules' { + interface ReactNativeFirebaseNativeModules { + RNFBStorageModule: RNFBStorageModule; + } +} diff --git a/packages/storage/lib/index.d.ts b/packages/storage/lib/types/namespaced.ts similarity index 85% rename from packages/storage/lib/index.d.ts rename to packages/storage/lib/types/namespaced.ts index ef617137fa..1217612692 100644 --- a/packages/storage/lib/index.d.ts +++ b/packages/storage/lib/types/namespaced.ts @@ -15,7 +15,7 @@ * */ -import { ReactNativeFirebase } from '@react-native-firebase/app'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; /** * Firebase Cloud Storage package for React Native. @@ -53,13 +53,20 @@ import { ReactNativeFirebase } from '@react-native-firebase/app'; * * @firebase storage */ +/** + * @deprecated Use the exported types directly instead. + * FirebaseStorageTypes namespace is kept for backwards compatibility. + */ +/* eslint-disable @typescript-eslint/no-namespace */ export namespace FirebaseStorageTypes { - import FirebaseModule = ReactNativeFirebase.FirebaseModule; - import NativeFirebaseError = ReactNativeFirebase.NativeFirebaseError; + type FirebaseModule = ReactNativeFirebase.FirebaseModule; + type NativeFirebaseError = ReactNativeFirebase.NativeFirebaseError; /** * Possible string formats used for uploading via `StorageReference.putString()` * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. + * * ```js * firebase.storage.StringFormat; * ``` @@ -143,19 +150,26 @@ export namespace FirebaseStorageTypes { * * Event subscription is created via `StorageTask.on()`. * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. + * * ```js * firebase.storage.TaskEvent; * ``` */ - export interface TaskEvent { - /** - * An event that indicates that the tasks state has changed. - * - * ```js - * firebase.storage.TaskEvent.STATE_CHANGED; - * ``` - */ - STATE_CHANGED: 'state_changed'; + /** + * An event that is triggered on a task. + * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. + */ + export type TaskEvent = 'state_changed'; + + /** + * Static properties for task events (runtime object). + * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. + */ + export interface TaskEventStatic { + STATE_CHANGED: TaskEvent; } /** @@ -163,6 +177,8 @@ export namespace FirebaseStorageTypes { * * An event subscription is created via `StorageTask.on()`. * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. + * * ```js * firebase.storage.TaskEvent; * ``` @@ -171,7 +187,7 @@ export namespace FirebaseStorageTypes { /** * Task has been cancelled by the user. */ - CANCELLED: 'cancelled'; + CANCELLED: 'canceled'; /** * An Error occurred, see TaskSnapshot.error for details. @@ -194,9 +210,20 @@ export namespace FirebaseStorageTypes { SUCCESS: 'success'; } + /** + * Represents the current state of a running upload. + * + * Note: The Firebase JS SDK uses "canceled" (one L). React Native Firebase historically used "cancelled" (two L). + * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. + */ + export type TaskStateType = 'running' | 'paused' | 'success' | 'canceled' | 'error'; + /** * Cloud Storage statics. * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. + * * #### Example * * ```js @@ -235,14 +262,16 @@ export namespace FirebaseStorageTypes { * firebase.storage.TaskEvent; * ``` */ - TaskEvent: TaskEvent; + TaskEvent: TaskEventStatic; SDK_VERSION: string; } /** - * An interface representing all the metadata properties that can be set. + * Object metadata that can be set at any time. * * This is used in updateMetadata, put, putString & putFile. + * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. */ export interface SettableMetadata { /** @@ -270,28 +299,28 @@ export namespace FirebaseStorageTypes { * * [Learn more about this header on Mozilla.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control) */ - cacheControl?: string | null; + cacheControl?: string | undefined; /** * The 'Content-Disposition' HTTP header that will be set on the storage object when it's requested. * * [Learn more about this header on Mozilla.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) */ - contentDisposition?: string | null; + contentDisposition?: string | undefined; /** * The 'Content-Encoding' HTTP header that will be used on the storage object when it's requested. * * [Learn more about this header on Mozilla.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding) */ - contentEncoding?: string | null; + contentEncoding?: string | undefined; /** * The 'Content-Language' HTTP header that will be set on the storage object when it's requested. * * [Learn more about this header on Mozilla.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language) */ - contentLanguage?: string | null; + contentLanguage?: string | undefined; /** * The 'Content-Type' HTTP header that will be set on the object when it's requested. @@ -314,17 +343,12 @@ export namespace FirebaseStorageTypes { * * [Learn more about this header on Mozilla.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) */ - contentType?: string | null; - - /** - * You may specify the md5hash of the file in metadata on upload only. It may not be updated via updateMetadata - */ - md5hash?: string | null; + contentType?: string | undefined; /** * Additional user-defined custom metadata for this storage object. * - * All values must be strings. Set to null to delete all. Any keys ommitted during update will be removed. + * All values must be strings. * * #### Example * @@ -337,20 +361,31 @@ export namespace FirebaseStorageTypes { * }, * } */ - customMetadata?: { - [key: string]: string; - } | null; + customMetadata?: + | { + [key: string]: string; + } + | undefined; } /** - * The full readable metadata returned by `TaskSnapshot.metadata` or `StorageReference.getMetadata()`. + * Object metadata that can be set at upload. + * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. */ - export interface FullMetadata extends SettableMetadata { + export interface UploadMetadata extends SettableMetadata { /** - * A Base64-encoded MD5 hash of the storage object being uploaded. + * A Base64-encoded MD5 hash of the object being uploaded. */ - md5Hash: string | null; + md5Hash?: string | undefined; + } + /** + * The full readable metadata returned by `TaskSnapshot.metadata` or `StorageReference.getMetadata()`. + * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. + */ + export interface FullMetadata extends UploadMetadata { /** * The bucket this storage object is contained in. * @@ -424,6 +459,16 @@ export namespace FirebaseStorageTypes { * ``` */ updated: string; + + /** + * Tokens to allow access to the download URL. + */ + downloadTokens: string[] | undefined; + + /** + * `StorageReference` associated with this upload. + */ + ref?: Reference | undefined; } /** @@ -431,6 +476,8 @@ export namespace FirebaseStorageTypes { * * A reference can be used to upload and download storage objects, get/set storage object metadata, retrieve storage object download urls and delete storage objects. * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. + * * #### Example 1 * * Get a reference to a specific storage path. @@ -594,9 +641,9 @@ export namespace FirebaseStorageTypes { * ``` * * @param localFilePath The local file path to upload to the bucket at the reference location. - * @param metadata Any additional `SettableMetadata` for this task. + * @param metadata Any additional `UploadMetadata` for this task. */ - putFile(localFilePath: string, metadata?: SettableMetadata): Task; + putFile(localFilePath: string, metadata?: UploadMetadata): Task; /** * Downloads a file to the specified local file path on the device. @@ -629,7 +676,7 @@ export namespace FirebaseStorageTypes { * @param data The data to upload to the storage bucket at the reference location. * @param metadata */ - put(data: Blob | Uint8Array | ArrayBuffer, metadata?: SettableMetadata): Task; + put(data: Blob | Uint8Array | ArrayBuffer, metadata?: UploadMetadata): Task; /** * Puts a string on the storage bucket. Depending on the string type, set a {@link StringFormat} type. @@ -645,12 +692,12 @@ export namespace FirebaseStorageTypes { * * @param data The string data, must match the format provided. * @param format The format type of the string, e.g. a Base64 format string. - * @param metadata Any additional `SettableMetadata` for this task. + * @param metadata Any additional `UploadMetadata` for this task. */ putString( data: string, format?: 'raw' | 'base64' | 'base64url' | 'data_url', - metadata?: SettableMetadata, + metadata?: UploadMetadata, ): Task; /** @@ -677,6 +724,8 @@ export namespace FirebaseStorageTypes { * * #### Example * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. + * * ```js * const ref = firebase.storage().ref(...); * const task = ref.put(...) @@ -700,19 +749,19 @@ export namespace FirebaseStorageTypes { * * @param taskSnapshot A `TaskSnapshot` for the event. */ - next: (taskSnapshot: TaskSnapshot) => void; + next?: (taskSnapshot: TaskSnapshot) => void; /** * Called when the task errors. * * @param error A JavaScript error. */ - error: (error: NativeFirebaseError) => void; + error?: (error: NativeFirebaseError) => void; /** * Called when the task has completed successfully. */ - complete: () => void; + complete?: () => void; } /** @@ -720,6 +769,8 @@ export namespace FirebaseStorageTypes { * * #### Example 1 * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. + * * Get a Upload Storage task to upload a string: * * ```js @@ -859,6 +910,8 @@ export namespace FirebaseStorageTypes { * * #### Example 1 * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. + * * ```js * firebase * .storage() @@ -906,7 +959,7 @@ export namespace FirebaseStorageTypes { /** * The current state of the task snapshot. */ - state: 'cancelled' | 'error' | 'paused' | 'running' | 'success'; + state: TaskState; /** * The parent {@link Task} of this snapshot. @@ -927,6 +980,8 @@ export namespace FirebaseStorageTypes { /** * Result returned from a non-resumable upload. + * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. */ export interface TaskResult { /** @@ -942,6 +997,8 @@ export namespace FirebaseStorageTypes { /** * The options `list()` accepts. + * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. */ export interface ListOptions { /** @@ -952,11 +1009,13 @@ export namespace FirebaseStorageTypes { /** * The `nextPageToken` from a previous call to `list()`. If provided, listing is resumed from the previous position. */ - pageToken?: string; + pageToken?: string | null; } /** * Result returned by `list()`. + * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. */ export interface ListResult { /** @@ -979,6 +1038,8 @@ export namespace FirebaseStorageTypes { /** * Storage Emulator options. Web only. + * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. */ export interface EmulatorMockTokenOptions { /** @@ -992,6 +1053,8 @@ export namespace FirebaseStorageTypes { * * #### Example 1 * + * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. + * * Get the storage instance for the **default app**: * * ```js @@ -1020,7 +1083,7 @@ export namespace FirebaseStorageTypes { * ``` * */ - export class Module extends FirebaseModule { + export interface Module extends FirebaseModule { /** * The current `FirebaseApp` instance for this Firebase service. */ @@ -1158,7 +1221,7 @@ type StorageNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< declare const defaultExport: StorageNamespace; -export const firebase: ReactNativeFirebase.Module & { +export declare const firebase: ReactNativeFirebase.Module & { storage: typeof defaultExport; app(name?: string): ReactNativeFirebase.FirebaseApp & { storage(): FirebaseStorageTypes.Module }; }; @@ -1168,6 +1231,7 @@ export default defaultExport; /** * Attach namespace to `firebase.` and `FirebaseApp.`. */ + declare module '@react-native-firebase/app' { namespace ReactNativeFirebase { import FirebaseModuleWithStaticsAndApp = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; @@ -1182,5 +1246,3 @@ declare module '@react-native-firebase/app' { } } } - -export * from './modular'; diff --git a/packages/storage/lib/types/storage.ts b/packages/storage/lib/types/storage.ts new file mode 100644 index 0000000000..a35c76e6eb --- /dev/null +++ b/packages/storage/lib/types/storage.ts @@ -0,0 +1,417 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 type { FirebaseApp, ReactNativeFirebase } from '@react-native-firebase/app'; +import type { + CompleteFn, + NextFn, + Unsubscribe, +} from '@react-native-firebase/app/dist/module/types/common'; +export type { CompleteFn, NextFn, Unsubscribe }; +export type NativeFirebaseError = ReactNativeFirebase.NativeFirebaseError; +/** + * Storage module instance + */ +export interface FirebaseStorage { + /** The FirebaseApp this module is associated with */ + app: FirebaseApp; + + /** + * The maximum time in milliseconds to retry an upload if a failure occurs. + */ + readonly maxUploadRetryTime: number; + + /** + * The maximum time in milliseconds to retry a download if a failure occurs. + */ + readonly maxDownloadRetryTime: number; + + /** + * The maximum time in milliseconds to retry operations if a failure occurs. + */ + readonly maxOperationRetryTime: number; +} + +/** + * Storage reference to a file or folder location. + */ +export interface StorageReference { + /** + * A reference to the root of this object's bucket. + */ + root: StorageReference; + /** + * The name of the bucket containing this reference's object. + */ + bucket: string; + /** + * The full path of this object. + */ + fullPath: string; + /** + * The short name of this object, which is the last component of the full path. + * For example, if fullPath is 'full/path/image.png', name is 'image.png'. + */ + name: string; + /** + * The {@link FirebaseStorage} instance associated with this reference. + */ + storage: FirebaseStorage; + /** + * A reference pointing to the parent location of this reference, or null if + * this reference is the root. + */ + parent: StorageReference | null; + /** + * Returns the full URL string for this object in the form + * @returns The full URL string. + */ + toString(): string; +} + +/** + * Options for listing files and folders. + */ +export interface ListOptions { + /** + * If set, limits the total number of `prefixes` and `items` to return. + * The default and maximum maxResults is 1000. + */ + maxResults?: number | null; + /** + * The `nextPageToken` from a previous call to `list()`. If provided, + * listing is resumed from the previous position. + */ + pageToken?: string | null; +} + +/** + * Result of listing files and folders. + */ +export interface ListResult { + /** + * References to prefixes (sub-folders). You can call list() on them to + * get its contents. + * + * Folders are implicit based on '/' in the object paths. + * For example, if a bucket has two objects '/a/b/1' and '/a/b/2', list('/a') + * will return '/a/b' as a prefix. + */ + prefixes: StorageReference[]; + /** + * Objects in this directory. + * You can call getMetadata() and getDownloadUrl() on them. + */ + items: StorageReference[]; + /** + * If set, there might be more results for this list. Use this token to resume the list. + */ + nextPageToken?: string; +} + +/** + * Object metadata that can be set at any time. + */ +export interface SettableMetadata { + cacheControl?: string | undefined; + contentDisposition?: string | undefined; + contentEncoding?: string | undefined; + contentLanguage?: string | undefined; + contentType?: string | undefined; + customMetadata?: { [key: string]: string } | undefined; +} + +/** + * Object metadata that can be set at upload. + * @public + */ +export interface UploadMetadata extends SettableMetadata { + /** + * A Base64-encoded MD5 hash of the object being uploaded. + */ + md5Hash?: string | undefined; +} + +/** + * The full set of object metadata, including read-only properties. + */ +export interface FullMetadata extends UploadMetadata { + /** + * The bucket this object is contained in. + */ + bucket: string; + + /** + * The full path of this object. + */ + fullPath: string; + + /** + * The object's generation. + * {@link https://cloud.google.com/storage/docs/metadata#generation-number} + */ + generation: string; + + /** + * The object's metageneration. + * {@link https://cloud.google.com/storage/docs/metadata#generation-number} + */ + metageneration: string; + + /** + * The short name of this object, which is the last component of the full path. + * For example, if fullPath is 'full/path/image.png', name is 'image.png'. + */ + name: string; + + /** + * The size of this object, in bytes. + */ + size: number; + + /** + * A date string representing when this object was created. + */ + timeCreated: string; + + /** + * A date string representing when this object was last updated. + */ + updated: string; + + /** + * Tokens to allow access to the download URL. + */ + downloadTokens: string[] | undefined; + + /** + * `StorageReference` associated with this upload. + */ + ref?: StorageReference | undefined; +} + +/** + * An enumeration of the possible string formats for upload. + * @public + */ +export type StringFormat = (typeof StringFormat)[keyof typeof StringFormat]; + +/** + * An enumeration of the possible string formats for upload. + * @public + */ +export const StringFormat = { + RAW: 'raw', + BASE64: 'base64', + BASE64URL: 'base64url', + DATA_URL: 'data_url', +} as const; + +/** + * An event that is triggered on a task. + * @public + */ +export type TaskEvent = (typeof TaskEvent)[keyof typeof TaskEvent]; + +/** + * An event that is triggered on a task. + * @public + */ +export const TaskEvent = { + STATE_CHANGED: 'state_changed', +} as const; + +/** + * Represents the current state of a running upload/download. + * @public + */ +export type TaskState = (typeof TaskState)[keyof typeof TaskState]; + +/** + * Represents the current state of a running upload/download. + * @public + */ +export const TaskState = { + RUNNING: 'running', + PAUSED: 'paused', + SUCCESS: 'success', + CANCELED: 'canceled', + // Backwards-compat alias + CANCELLED: 'canceled', + ERROR: 'error', +} as const; + +/** + * A stream observer for Firebase Storage. + * @public + */ +export interface StorageObserver { + next?: NextFn | null; + error?: ((error: NativeFirebaseError) => void) | null; + complete?: CompleteFn | null; +} + +/** + * Represents the process of uploading an object. Allows you to monitor and manage the upload. + * + * Note: React Native Firebase returns Promises for pause/resume/cancel to communicate with native iOS/Android. + * + * @public + */ +export interface UploadTask { + /** + * Cancels a running task. Has no effect on a complete or failed task. + * @returns True if the cancel had an effect. + */ + cancel(): Promise; + + /** + * Equivalent to calling `then(null, onRejected)`. + */ + catch(onRejected: (error: NativeFirebaseError) => unknown): Promise; + + /** + * Listens for events on this task. + * + * @returns If only the event argument is passed, returns a function you can use to add callbacks. + * Otherwise returns a function you can call to unregister the callbacks. + */ + on( + event: TaskEvent, + nextOrObserver?: + | StorageObserver + | null + | ((snapshot: UploadTaskSnapshot) => unknown), + error?: ((error: NativeFirebaseError) => unknown) | null, + complete?: CompleteFn | null, + ): Unsubscribe | Subscribe; + + /** + * Pauses a currently running task. Has no effect on a paused or failed task. + * @returns True if the operation took effect, false if ignored. + */ + pause(): Promise; + + /** + * Resumes a paused task. Has no effect on a currently running or failed task. + * @returns True if the operation took effect, false if ignored. + */ + resume(): Promise; + + /** + * A snapshot of the current task state. + */ + snapshot: UploadTaskSnapshot; + + /** + * This object behaves like a Promise, and resolves with its snapshot data when the upload completes. + */ + then( + onFulfilled?: ((snapshot: UploadTaskSnapshot) => unknown) | null, + onRejected?: ((error: NativeFirebaseError) => unknown) | null, + ): Promise; +} + +/** + * Holds data about the current state of the upload task. + * @public + */ +export interface UploadTaskSnapshot { + /** + * The number of bytes that have been successfully uploaded so far. + */ + bytesTransferred: number; + + /** + * Before the upload completes, contains the metadata sent to the server. + * After the upload completes, contains the metadata sent back from the server. + */ + metadata: FullMetadata; + + /** + * The reference that spawned this snapshot's upload task. + */ + ref: StorageReference; + + /** + * The current state of the task. + */ + state: TaskState; + + /** + * The task of which this is a snapshot. + */ + task: UploadTask; + + /** + * The total number of bytes to be uploaded. + */ + totalBytes: number; +} + +/** + * Result returned from a non-resumable upload. + * @public + */ +export interface UploadResult { + /** + * Contains the metadata sent back from the server. + */ + readonly metadata: FullMetadata; + + /** + * The reference that spawned this upload. + */ + readonly ref: StorageReference; +} + +export type Subscribe = ( + nextOrObserver?: StorageObserver | null | NextFn, + error?: ((error: NativeFirebaseError) => unknown) | null, + complete?: CompleteFn | null, +) => Unsubscribe; + +/** + * Snapshot of a storage task (upload or download). + */ +export interface TaskSnapshot extends UploadTaskSnapshot { + /** + * If the state is `error`, returns a JavaScript error of the current task snapshot. + */ + error?: NativeFirebaseError; +} + +/** + * Result of a completed task. + */ +export type TaskResult = UploadResult; + +/** + * Observer object for task state changes. + */ +export type TaskSnapshotObserver = StorageObserver; + +/** + * Storage task for uploads or downloads. + */ +export type Task = UploadTask; + +/** + * Options for connecting to the storage emulator (web only). + */ +export interface EmulatorMockTokenOptions { + mockUserToken?: string | null; +} diff --git a/packages/storage/lib/utils.js b/packages/storage/lib/utils.ts similarity index 50% rename from packages/storage/lib/utils.js rename to packages/storage/lib/utils.ts index 07fb0c27d0..888f90be3c 100644 --- a/packages/storage/lib/utils.js +++ b/packages/storage/lib/utils.ts @@ -17,6 +17,9 @@ import { isNull, isObject, isString } from '@react-native-firebase/app/dist/module/common'; import { NativeFirebaseError } from '@react-native-firebase/app/dist/module/internal'; +import type { SettableMetadata, UploadMetadata } from './types/storage'; +import type { StorageInternal } from './types/internal'; +import type { NativeErrorUserInfo } from '@react-native-firebase/app/dist/module/types/internal'; const SETTABLE_FIELDS = [ 'cacheControl', @@ -25,21 +28,39 @@ const SETTABLE_FIELDS = [ 'contentLanguage', 'contentType', 'customMetadata', - 'md5hash', -]; - -export async function handleStorageEvent(storageInstance, event) { + 'md5Hash', +] as const; + +const LEGACY_MD5_HASH_FIELD = 'md5hash'; +const MD5_HASH_FIELD = 'md5Hash'; + +export async function handleStorageEvent( + storageInstance: StorageInternal, + event: { + taskId: string; + eventName: string; + body?: { error?: NativeErrorUserInfo }; + }, +): Promise { const { taskId, eventName } = event; const body = event.body || {}; if (body.error) { - body.error = await NativeFirebaseError.fromEvent(body.error, storageInstance._config.namespace); + // Convert NativeErrorUserInfo to NativeFirebaseError instance + const nativeError = NativeFirebaseError.fromEvent( + body.error, + storageInstance._config.namespace, + ); + // Assign NativeFirebaseError (Error instance) to body.error for consumers + // Type assertion needed because body.error is typed as NativeErrorUserInfo in input, + // but consumers expect Error instance + (body as { error?: Error }).error = nativeError; } storageInstance.emitter.emit(storageInstance.eventNameForApp(taskId, eventName), body); } -export function getHttpUrlParts(url) { +export function getHttpUrlParts(url: string): { bucket: string; path: string } | null { const decoded = decodeURIComponent(url); const parts = decoded.match(/\/b\/(.*)\/o\/([a-zA-Z0-9./\-_]+)(.*)/); @@ -47,10 +68,10 @@ export function getHttpUrlParts(url) { return null; } - return { bucket: `gs://${parts[1]}`, path: parts[2] }; + return { bucket: `gs://${parts[1]}`, path: parts[2]! }; } -export function getGsUrlParts(url) { +export function getGsUrlParts(url: string): { bucket: string; path: string } { const bucket = url.substring(0, url.indexOf('/', 5)) || url; const path = (url.indexOf('/', 5) > -1 ? url.substring(url.indexOf('/', 5) + 1, url.length) : '/') || '/'; @@ -58,31 +79,49 @@ export function getGsUrlParts(url) { return { bucket, path }; } -export function validateMetadata(metadata, update = true) { +export function validateMetadata( + metadata: SettableMetadata | UploadMetadata, + update = true, +): SettableMetadata | UploadMetadata { if (!isObject(metadata)) { throw new Error('firebase.storage.SettableMetadata must be an object value if provided.'); } const metadataEntries = Object.entries(metadata); + const validatedMetadata: Record = {}; + let hasLegacyMd5Hash = false; + let hasMd5Hash = false; for (let i = 0; i < metadataEntries.length; i++) { - const [key, value] = metadataEntries[i]; + const entry = metadataEntries[i]; + if (!entry) continue; + const [key, value] = entry; + const normalizedKey = key === LEGACY_MD5_HASH_FIELD ? MD5_HASH_FIELD : key; + + if (key === LEGACY_MD5_HASH_FIELD) { + hasLegacyMd5Hash = true; + } + + if (key === MD5_HASH_FIELD) { + hasMd5Hash = true; + } + // validate keys - if (!SETTABLE_FIELDS.includes(key)) { + if (!SETTABLE_FIELDS.includes(normalizedKey as (typeof SETTABLE_FIELDS)[number])) { throw new Error( `firebase.storage.SettableMetadata unknown property '${key}' provided for metadata.`, ); } // md5 is only allowed on put, not on update - if (key === 'md5hash' && update === true) { + if (normalizedKey === MD5_HASH_FIELD && update === true) { throw new Error( - `firebase.storage.SettableMetadata md5hash may only be set on upload, not on updateMetadata`, + `firebase.storage.SettableMetadata ${MD5_HASH_FIELD} may only be set on upload, not on updateMetadata`, ); } // validate values - if (key !== 'customMetadata') { + if (normalizedKey !== 'customMetadata') { if (!isString(value) && !isNull(value)) { throw new Error( `firebase.storage.SettableMetadata invalid property '${key}' should be a string or null value.`, @@ -93,7 +132,15 @@ export function validateMetadata(metadata, update = true) { 'firebase.storage.SettableMetadata.customMetadata must be an object of keys and string values or null value.', ); } + + validatedMetadata[normalizedKey] = value; + } + + if (hasLegacyMd5Hash && hasMd5Hash) { + throw new Error( + "firebase.storage.SettableMetadata cannot contain both 'md5Hash' and legacy 'md5hash' properties.", + ); } - return metadata; + return validatedMetadata as SettableMetadata | UploadMetadata; } diff --git a/packages/storage/lib/web/RNFBStorageModule.android.js b/packages/storage/lib/web/RNFBStorageModule.android.js deleted file mode 100644 index af77c859b1..0000000000 --- a/packages/storage/lib/web/RNFBStorageModule.android.js +++ /dev/null @@ -1,2 +0,0 @@ -// No-op for android. -export default {}; diff --git a/packages/storage/lib/web/RNFBStorageModule.android.ts b/packages/storage/lib/web/RNFBStorageModule.android.ts new file mode 100644 index 0000000000..eeef32701e --- /dev/null +++ b/packages/storage/lib/web/RNFBStorageModule.android.ts @@ -0,0 +1,3 @@ +// No-op for android. +const RNFBStorageModule = {}; +export default RNFBStorageModule; diff --git a/packages/storage/lib/web/RNFBStorageModule.ios.js b/packages/storage/lib/web/RNFBStorageModule.ios.js deleted file mode 100644 index a3429ada0e..0000000000 --- a/packages/storage/lib/web/RNFBStorageModule.ios.js +++ /dev/null @@ -1,2 +0,0 @@ -// No-op for ios. -export default {}; diff --git a/packages/storage/lib/web/RNFBStorageModule.ios.ts b/packages/storage/lib/web/RNFBStorageModule.ios.ts new file mode 100644 index 0000000000..47cf3864ae --- /dev/null +++ b/packages/storage/lib/web/RNFBStorageModule.ios.ts @@ -0,0 +1,3 @@ +// Re-export the main module +const RNFBStorageModule = {}; +export default RNFBStorageModule; diff --git a/packages/storage/lib/web/RNFBStorageModule.js b/packages/storage/lib/web/RNFBStorageModule.ts similarity index 57% rename from packages/storage/lib/web/RNFBStorageModule.js rename to packages/storage/lib/web/RNFBStorageModule.ts index b58cc8491c..62d1486f97 100644 --- a/packages/storage/lib/web/RNFBStorageModule.js +++ b/packages/storage/lib/web/RNFBStorageModule.ts @@ -12,6 +12,15 @@ import { uploadBytesResumable, ref as firebaseStorageRef, } from '@react-native-firebase/app/dist/module/internal/web/firebaseStorage'; +import type { + StorageReference, + UploadTask, + UploadTaskSnapshot as FirebaseUploadTaskSnapshot, + FullMetadata, + ListResult as FirebaseListResult, +} from '@react-native-firebase/app/dist/module/internal/web/firebaseStorage'; +import type { FirebaseApp } from '@react-native-firebase/app/dist/module/internal/web/firebaseApp'; + import { guard, getWebError, @@ -19,17 +28,62 @@ import { } from '@react-native-firebase/app/dist/module/internal/web/utils'; import { Base64 } from '@react-native-firebase/app/dist/module/common'; -function rejectWithCodeAndMessage(code, message) { - return Promise.reject( - getWebError({ - code, - message, - }), - ); +type Storage = ReturnType; +interface EmulatorConfig { + host: string; + port: number; +} + +interface Metadata { + bucket?: string; + generation?: string; + metageneration?: string; + fullPath?: string; + name?: string; + size?: number; + timeCreated?: string; + updated?: string; + md5Hash?: string; + cacheControl?: string; + contentLanguage?: string; + contentDisposition?: string; + contentEncoding?: string; + contentType?: string; + customMetadata?: Record; +} + +interface SettableMetadata { + cacheControl?: string; + contentDisposition?: string; + contentEncoding?: string; + contentType?: string; + contentLanguage?: string; + customMetadata?: Record; + md5Hash?: string; +} + +interface UploadTaskSnapshot { + totalBytes: number; + bytesTransferred: number; + state: string; + metadata: Metadata; +} + +interface ListOptions { + maxResults?: number; + pageToken?: string | null; +} + +function rejectWithCodeAndMessage(code: string, message: string): Promise { + const error = new Error(message); + (error as Error & { code?: string }).code = code; + return Promise.reject(getWebError(error)); } -function metadataToObject(metadata) { - const out = { +function metadataToObject( + metadata: Metadata | FullMetadata, +): Metadata & { metadata?: Record } { + const out: Metadata & { metadata?: Record } = { bucket: metadata.bucket, generation: metadata.generation, metageneration: metadata.metageneration, @@ -70,7 +124,10 @@ function metadataToObject(metadata) { return out; } -function uploadTaskErrorToObject(error, snapshot) { +function uploadTaskErrorToObject( + error: Error, + snapshot: FirebaseUploadTaskSnapshot | null, +): UploadTaskSnapshot & { error: ReturnType } { return { ...uploadTaskSnapshotToObject(snapshot), state: 'error', @@ -78,28 +135,18 @@ function uploadTaskErrorToObject(error, snapshot) { }; } -function uploadTaskSnapshotToObject(snapshot) { +function uploadTaskSnapshotToObject( + snapshot: FirebaseUploadTaskSnapshot | null, +): UploadTaskSnapshot { return { totalBytes: snapshot ? snapshot.totalBytes : 0, bytesTransferred: snapshot ? snapshot.bytesTransferred : 0, - state: snapshot ? taskStateToString(snapshot.state) : 'unknown', - metadata: snapshot ? metadataToObject(snapshot.metadata) : {}, + state: snapshot ? snapshot.state : 'unknown', + metadata: snapshot && snapshot.metadata ? metadataToObject(snapshot.metadata as Metadata) : {}, }; } -function taskStateToString(state) { - const override = { - canceled: 'cancelled', - }; - - if (state in override) { - return override[state]; - } - - return state; -} - -function makeSettableMetadata(metadata) { +function makeSettableMetadata(metadata: SettableMetadata): SettableMetadata { return { cacheControl: metadata.cacheControl, contentDisposition: metadata.contentDisposition, @@ -110,74 +157,79 @@ function makeSettableMetadata(metadata) { }; } -function listResultToObject(result) { +function listResultToObject(result: FirebaseListResult) { return { - nextPageToken: result.nextPageToken, + nextPageToken: result.nextPageToken ?? undefined, items: result.items.map(ref => ref.fullPath), prefixes: result.prefixes.map(ref => ref.fullPath), }; } -const emulatorForApp = {}; -const appInstances = {}; -const storageInstances = {}; -const tasks = {}; +const emulatorForApp: Record = {}; +const appInstances: Record = {}; +const storageInstances: Record = {}; +const tasks: Record = {}; -function getBucketFromUrl(url) { +function getBucketFromUrl(url: string): string { // Check if the URL starts with "gs://" if (url.startsWith('gs://')) { // Return the bucket name by extracting everything up to the first slash after "gs://" // and removing the "gs://" prefix - return url.substring(5).split('/')[0]; + return url.substring(5).split('/')[0]!; } else { // Assume the URL is a path format, strip the leading slash if it exists and extract the bucket name const strippedUrl = url.startsWith('/') ? url.substring(1) : url; // Extract the bucket from the path, assuming it ends at the first slash after the domain - return strippedUrl.split('/')[0]; + return strippedUrl.split('/')[0]!; } } -function getCachedAppInstance(appName) { +function getCachedAppInstance(appName: string): FirebaseApp { return (appInstances[appName] ??= getApp(appName)); } -function emulatorKey(appName, url) { +function emulatorKey(appName: string, url: string): string { const bucket = getBucketFromUrl(url); return `${appName}|${bucket}`; } // Returns a cached Storage instance. -function getCachedStorageInstance(appName, url) { - let instance; - const bucket = url ? getBucketFromUrl(url) : getCachedAppInstance(appName).options.storageBucket; - - if (!url) { - instance = getCachedStorageInstance( - appName, - getCachedAppInstance(appName).options.storageBucket, - ); +function getCachedStorageInstance(appName: string, url?: string | null): Storage { + let instance: Storage; + const app = getCachedAppInstance(appName); + const bucket = url ? getBucketFromUrl(url) : (app.options.storageBucket ?? undefined); + + if (!url || !bucket) { + const defaultBucket = app.options.storageBucket; + if (!defaultBucket) { + throw new Error(`No storage bucket configured for app ${appName}`); + } + instance = getCachedStorageInstance(appName, defaultBucket); } else { - instance = storageInstances[`${appName}|${bucket}`] ??= getStorage( - getCachedAppInstance(appName), - bucket, - ); + instance = storageInstances[`${appName}|${bucket}`] ??= getStorage(app, bucket); } - const key = emulatorKey(appName, bucket); - if (emulatorForApp[key]) { - connectStorageEmulator(instance, emulatorForApp[key].host, emulatorForApp[key].port); + if (bucket) { + const key = emulatorKey(appName, bucket); + if (emulatorForApp[key]) { + connectStorageEmulator(instance, emulatorForApp[key].host, emulatorForApp[key].port); + } } return instance; } // Returns a Storage Reference. -function getReferenceFromUrl(appName, url) { +function getReferenceFromUrl(appName: string, url: string): StorageReference { const path = url.substring(url.indexOf('/') + 1); const instance = getCachedStorageInstance(appName, path); return firebaseStorageRef(instance, url); } -const CONSTANTS = {}; +const CONSTANTS: { + maxDownloadRetryTime?: number; + maxOperationRetryTime?: number; + maxUploadRetryTime?: number; +} = {}; const defaultAppInstance = getApps()[0]; if (defaultAppInstance) { @@ -191,11 +243,11 @@ export default { /** * Delete an object at the path. - * @param {string} appName - The app name. - * @param {string} url - The path to the object. + * @param appName - The app name. + * @param url - The path to the object. * @return {Promise} */ - delete(appName, url) { + delete(appName: string, url: string): Promise { return guard(async () => { const ref = getReferenceFromUrl(appName, url); await deleteObject(ref); @@ -204,11 +256,11 @@ export default { /** * Get the download URL for an object. - * @param {string} appName - The app name. - * @param {string} url - The path to the object. + * @param appName - The app name. + * @param url - The path to the object. * @return {Promise} The download URL. */ - getDownloadURL(appName, url) { + getDownloadURL(appName: string, url: string): Promise { return guard(async () => { const ref = getReferenceFromUrl(appName, url); const downloadURL = await getDownloadURL(ref); @@ -218,11 +270,14 @@ export default { /** * Get the metadata for an object. - * @param {string} appName - The app name. - * @param {string} url - The path to the object. + * @param appName - The app name. + * @param url - The path to the object. * @return {Promise} The metadata. */ - getMetadata(appName, url) { + getMetadata( + appName: string, + url: string, + ): Promise }> { return guard(async () => { const ref = getReferenceFromUrl(appName, url); const metadata = await getMetadata(ref); @@ -232,12 +287,20 @@ export default { /** * List objects at the path. - * @param {string} appName - The app name. - * @param {string} url - The path to the object. - * @param {Object} listOptions - The list options. + * @param appName - The app name. + * @param url - The path to the object. + * @param listOptions - The list options. * @return {Promise} The list result. */ - list(appName, url, listOptions) { + list( + appName: string, + url: string, + listOptions?: ListOptions, + ): Promise<{ + nextPageToken?: string; + items: string[]; + prefixes: string[]; + }> { return guard(async () => { const ref = getReferenceFromUrl(appName, url); const listResult = await list(ref, listOptions); @@ -247,11 +310,18 @@ export default { /** * List all objects at the path. - * @param {string} appName - The app name. - * @param {string} url - The path to the object. + * @param appName - The app name. + * @param url - The path to the object. * @return {Promise} The list result. */ - listAll(appName, url) { + listAll( + appName: string, + url: string, + ): Promise<{ + nextPageToken?: string; + items: string[]; + prefixes: string[]; + }> { return guard(async () => { const ref = getReferenceFromUrl(appName, url); const listResult = await listAll(ref); @@ -261,11 +331,15 @@ export default { /** * Update the metadata for an object. - * @param {string} appName - The app name. - * @param {string} url - The path to the object. - * @param {Object} metadata - The metadata (SettableMetadata). + * @param appName - The app name. + * @param url - The path to the object. + * @param metadata - The metadata (SettableMetadata). */ - updateMetadata(appName, url, metadata) { + updateMetadata( + appName: string, + url: string, + metadata: SettableMetadata, + ): Promise }> { return guard(async () => { const ref = getReferenceFromUrl(appName, url); const updated = await updateMetadata(ref, makeSettableMetadata(metadata)); @@ -273,7 +347,7 @@ export default { }); }, - setMaxDownloadRetryTime() { + setMaxDownloadRetryTime(): void { if (__DEV__) { // eslint-disable-next-line no-console console.warn( @@ -285,11 +359,11 @@ export default { /** * Set the maximum operation retry time. - * @param {string} appName - The app name. - * @param {number} milliseconds - The maximum operation retry time. + * @param appName - The app name. + * @param milliseconds - The maximum operation retry time. * @return {Promise} */ - setMaxOperationRetryTime(appName, milliseconds) { + setMaxOperationRetryTime(appName: string, milliseconds: number): Promise { return guard(async () => { const storage = getCachedStorageInstance(appName); storage.maxOperationRetryTime = milliseconds; @@ -298,11 +372,11 @@ export default { /** * Set the maximum upload retry time. - * @param {string} appName - The app name. - * @param {number} milliseconds - The maximum upload retry time. + * @param appName - The app name. + * @param milliseconds - The maximum upload retry time. * @return {Promise} */ - setMaxUploadRetryTime(appName, milliseconds) { + setMaxUploadRetryTime(appName: string, milliseconds: number): Promise { return guard(async () => { const storage = getCachedStorageInstance(appName); storage.maxUploadRetryTime = milliseconds; @@ -311,12 +385,12 @@ export default { /** * Use the Firebase Storage emulator. - * @param {string} appName - The app name. - * @param {string} host - The emulator host. - * @param {number} port - The emulator port. + * @param appName - The app name. + * @param host - The emulator host. + * @param port - The emulator port. * @return {Promise} */ - useEmulator(appName, host, port, url) { + useEmulator(appName: string, host: string, port: number, url: string): Promise { return guard(async () => { const instance = getCachedStorageInstance(appName, url); connectStorageEmulator(instance, host, port); @@ -325,7 +399,7 @@ export default { }); }, - writeToFile() { + writeToFile(): Promise { return rejectWithCodeAndMessage( 'unsupported', 'This operation is not supported in this environment.', @@ -334,18 +408,25 @@ export default { /** * Put a string to the path. - * @param {string} appName - The app name. - * @param {string} url - The path to the object. - * @param {string} string - The string to put. - * @param {string} format - The format of the string. - * @param {Object} metadata - The metadata (SettableMetadata). - * @param {string} taskId - The task ID. + * @param appName - The app name. + * @param url - The path to the object. + * @param string - The string to put. + * @param format - The format of the string. + * @param metadata - The metadata (SettableMetadata). + * @param taskId - The task ID. * @return {Promise} The upload snapshot. */ - putString(appName, url, string, format, metadata = {}, taskId) { + putString( + appName: string, + url: string, + string: string, + format: string, + metadata: SettableMetadata = {}, + taskId: string, + ): Promise { return guard(async () => { const ref = getReferenceFromUrl(appName, url); - let decodedString = null; + let decodedString: string | null = null; // This is always either base64 or base64url switch (format) { @@ -357,7 +438,7 @@ export default { break; } - const arrayBuffer = new Uint8Array([...decodedString].map(c => c.charCodeAt(0))); + const arrayBuffer = new Uint8Array([...decodedString!].map(c => c.charCodeAt(0))); const task = uploadBytesResumable(ref, arrayBuffer, { ...makeSettableMetadata(metadata), @@ -367,10 +448,10 @@ export default { // Store the task in the tasks map. tasks[taskId] = task; - const snapshot = await new Promise((resolve, reject) => { + const snapshot = await new Promise((resolve, reject) => { task.on( 'state_changed', - snapshot => { + (snapshot: FirebaseUploadTaskSnapshot) => { const event = { body: uploadTaskSnapshotToObject(snapshot), appName, @@ -379,7 +460,7 @@ export default { }; emitEvent('storage_event', event); }, - error => { + (error: Error) => { const errorSnapshot = uploadTaskErrorToObject(error, task.snapshot); const event = { body: { @@ -402,7 +483,7 @@ export default { delete tasks[taskId]; const event = { body: { - ...uploadTaskSnapshotToObject(snapshot), + ...uploadTaskSnapshotToObject(task.snapshot), state: 'success', }, appName, @@ -423,7 +504,7 @@ export default { }); }, - putFile() { + putFile(): Promise { return rejectWithCodeAndMessage( 'unsupported', 'This operation is not supported in this environment.', @@ -432,12 +513,12 @@ export default { /** * Set the status of a task. - * @param {string} appName - The app name. - * @param {string} taskId - The task ID. - * @param {number} status - The status. + * @param appName - The app name. + * @param taskId - The task ID. + * @param status - The status. * @return {Promise} Whether the status was set. */ - setTaskStatus(appName, taskId, status) { + setTaskStatus(appName: string, taskId: string, status: number): Promise { // TODO this function implementation cannot // be tested right now since we're unable // to create a big enough upload to be able to @@ -465,7 +546,7 @@ export default { } emitEvent('storage_event', { - data: buildUploadSnapshotMap(task.snapshot), + data: uploadTaskSnapshotToObject(task.snapshot), appName, taskId, }); diff --git a/packages/storage/package.json b/packages/storage/package.json index 1a5e052698..47c3a09512 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -3,12 +3,13 @@ "version": "24.0.0", "author": "Invertase (http://invertase.io)", "description": "React Native Firebase - React Native Firebase provides native integration with Cloud Storage, providing support to upload and download files directly from your device and from your Firebase Cloud Storage bucket.", - "main": "lib/index.js", - "types": "lib/index.d.ts", + "main": "./dist/module/index.js", + "types": "./dist/typescript/lib/index.d.ts", "scripts": { - "build": "genversion --semi lib/version.js", + "build": "genversion --esm --semi lib/version.ts", "build:clean": "rimraf android/build && rimraf ios/build", - "prepare": "yarn run build" + "compile": "bob build", + "prepare": "yarn run build && yarn compile" }, "repository": { "type": "git", @@ -34,5 +35,38 @@ "publishConfig": { "access": "public", "provenance": true - } + }, + "devDependencies": { + "react-native-builder-bob": "^0.40.13" + }, + "exports": { + ".": { + "source": "./lib/index.ts", + "types": "./dist/typescript/lib/index.d.ts", + "default": "./dist/module/index.js" + }, + "./package.json": "./package.json" + }, + "react-native-builder-bob": { + "source": "lib", + "output": "dist", + "targets": [ + [ + "module", + { + "esm": true + } + ], + [ + "typescript", + { + "tsc": "../../node_modules/.bin/tsc" + } + ] + ] + }, + "eslintIgnore": [ + "node_modules/", + "dist/" + ] } diff --git a/packages/storage/tsconfig.json b/packages/storage/tsconfig.json new file mode 100644 index 0000000000..48793d3809 --- /dev/null +++ b/packages/storage/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.packages.base.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": ".", + "paths": { + "@react-native-firebase/app/dist/module/common/*": ["../app/dist/typescript/lib/common/*"], + "@react-native-firebase/app/dist/module/common": ["../app/dist/typescript/lib/common"], + "@react-native-firebase/app/dist/module/internal/web/*": [ + "../app/dist/typescript/lib/internal/web/*" + ], + "@react-native-firebase/app/dist/module/internal/*": [ + "../app/dist/typescript/lib/internal/*" + ], + "@react-native-firebase/app/dist/module/internal": ["../app/dist/typescript/lib/internal"], + "@react-native-firebase/app": ["../app/dist/typescript/lib"], + "@react-native-firebase/app/dist/module/types/internal": ["../app/dist/typescript/lib/types/internal"], + "@react-native-firebase/app/dist/module/types/common": ["../app/dist/typescript/lib/types/common"], + } + }, + "include": ["lib/**/*"], + "exclude": ["node_modules", "dist", "__tests__", "**/*.test.ts"] +} diff --git a/packages/storage/type-test.ts b/packages/storage/type-test.ts index ff05532e3c..b5b753752e 100644 --- a/packages/storage/type-test.ts +++ b/packages/storage/type-test.ts @@ -1,9 +1,11 @@ import storage, { firebase, - FirebaseStorageTypes, + // Types + type FirebaseStorage, + type FirebaseStorageTypes, + // Modular API getStorage, connectStorageEmulator, - ref, deleteObject, getBlob, getBytes, @@ -16,18 +18,17 @@ import storage, { uploadBytes, uploadBytesResumable, uploadString, - refFromURL, + ref, setMaxOperationRetryTime, setMaxUploadRetryTime, putFile, writeToFile, - toString as storageToString, - child, setMaxDownloadRetryTime, StringFormat, TaskEvent, TaskState, } from '.'; +import { FullMetadata, ListResult, TaskResult, TaskSnapshot } from './lib/types/storage'; console.log(storage().app); @@ -210,29 +211,29 @@ getDownloadURL(modularRef1).then((url: string) => { console.log(url); }); -getMetadata(modularRef1).then((metadata: FirebaseStorageTypes.FullMetadata) => { +getMetadata(modularRef1).then((metadata: FullMetadata) => { console.log(metadata); }); const modularStream = getStream(modularRef1); console.log(modularStream); -list(modularRef1, { maxResults: 10 }).then((result: FirebaseStorageTypes.ListResult) => { +list(modularRef1, { maxResults: 10 }).then((result: ListResult) => { console.log(result.items); }); -listAll(modularRef1).then((result: FirebaseStorageTypes.ListResult) => { +listAll(modularRef1).then((result: ListResult) => { console.log(result.items); }); updateMetadata(modularRef1, { cacheControl: 'no-cache' }).then( - (metadata: FirebaseStorageTypes.FullMetadata) => { + (metadata: FullMetadata) => { console.log(metadata); }, ); uploadBytes(modularRef1, new Blob(), { cacheControl: 'no-cache' }).then( - (result: FirebaseStorageTypes.TaskResult) => { + (result: TaskResult) => { console.log(result); }, ); @@ -247,11 +248,11 @@ modularUploadBytesResumable.pause().then(() => { const modularUploadString = uploadString(modularRef1, 'modular-data', 'base64', { cacheControl: 'no-cache', }); -modularUploadString.then((snapshot: FirebaseStorageTypes.TaskSnapshot) => { +modularUploadString.then((snapshot: TaskSnapshot) => { console.log(snapshot); }); -const modularRefFromURL = refFromURL(modularStorage1, 'gs://bucket/path'); +const modularRefFromURL = ref(modularStorage1, 'gs://bucket/path'); console.log(modularRefFromURL.fullPath); setMaxOperationRetryTime(modularStorage1, 5000).then(() => { @@ -263,18 +264,18 @@ setMaxUploadRetryTime(modularStorage1, 25000).then(() => { }); const modularPutFile = putFile(modularRef1, '/local/path', { cacheControl: 'no-cache' }); -modularPutFile.then((snapshot: FirebaseStorageTypes.TaskSnapshot) => { +modularPutFile.then((snapshot: TaskSnapshot) => { console.log(snapshot); }); const modularWriteToFile = writeToFile(modularRef1, '/local/path'); -modularWriteToFile.then((snapshot: FirebaseStorageTypes.TaskSnapshot) => { +modularWriteToFile.then((snapshot: TaskSnapshot) => { console.log(snapshot); }); -console.log(storageToString(modularRef1)); +console.log(modularRef1.toString()); -const modularChildRef = child(modularRef1, 'child'); +const modularChildRef = ref(modularRef1, 'child'); console.log(modularChildRef.fullPath); setMaxDownloadRetryTime(modularStorage1, 25000).then(() => { @@ -285,3 +286,29 @@ setMaxDownloadRetryTime(modularStorage1, 25000).then(() => { console.log(StringFormat.RAW); console.log(TaskEvent.STATE_CHANGED); console.log(TaskState.SUCCESS); + +// Test type usage +const storageInstance2: FirebaseStorage = firebase.storage(); +console.log(storageInstance2.app.name); + +// Test backwards compatibility types +const legacyType: FirebaseStorageTypes.Module = firebase.storage(); +console.log(legacyType.app.name); +const legacyReference: FirebaseStorageTypes.Reference = firebase.storage().ref('test'); +console.log(legacyReference.fullPath); +const legacyMetadata: FirebaseStorageTypes.FullMetadata = {} as FirebaseStorageTypes.FullMetadata; +console.log(legacyMetadata); +const legacySettableMetadata: FirebaseStorageTypes.SettableMetadata = + {} as FirebaseStorageTypes.SettableMetadata; +console.log(legacySettableMetadata); +const legacyListResult: FirebaseStorageTypes.ListResult = {} as FirebaseStorageTypes.ListResult; +console.log(legacyListResult); +const legacyTaskSnapshot: FirebaseStorageTypes.TaskSnapshot = + {} as FirebaseStorageTypes.TaskSnapshot; +console.log(legacyTaskSnapshot); +const legacyTask: FirebaseStorageTypes.Task = {} as FirebaseStorageTypes.Task; +console.log(legacyTask); + +// Test SDK_VERSION +const sdkVersion: string = storage.SDK_VERSION; +console.log(sdkVersion); diff --git a/tests/local-tests/index.js b/tests/local-tests/index.js index 6d96524dfe..bfa2faf08a 100644 --- a/tests/local-tests/index.js +++ b/tests/local-tests/index.js @@ -31,6 +31,7 @@ import { PipelinesE2ETestComponent } from './firestore/pipelines-e2e'; import { VertexAITestComponent } from './vertexai/vertexai'; import { AuthMFADemonstrator } from './auth/auth-mfa-demonstrator'; import { HttpsCallableTestComponent } from './functions/https-callable'; +import { StorageTestComponent } from './storage/storage'; import { StreamingCallableTestComponent } from './functions/streaming-callable'; const testComponents = { @@ -45,6 +46,7 @@ const testComponents = { 'VertexAI Generation Example': VertexAITestComponent, 'Auth MFA Demonstrator': AuthMFADemonstrator, 'HttpsCallable Test': HttpsCallableTestComponent, + 'Storage Test': StorageTestComponent, 'Streaming Callable Test': StreamingCallableTestComponent, }; diff --git a/tests/local-tests/storage/storage.tsx b/tests/local-tests/storage/storage.tsx new file mode 100644 index 0000000000..f77f882619 --- /dev/null +++ b/tests/local-tests/storage/storage.tsx @@ -0,0 +1,346 @@ +import React, { useState } from 'react'; +import { Button, Text, View, ScrollView, StyleSheet } from 'react-native'; + +import { getApp, utils } from '@react-native-firebase/app'; + +import { + getStorage, + connectStorageEmulator, + ref, + uploadString, + writeToFile, + putFile, + TaskState, +} from '@react-native-firebase/storage'; + +const storage = getStorage(); +connectStorageEmulator(storage, 'localhost', 9199); + +const secondStorageBucket = 'gs://react-native-firebase-testing'; +const secondStorage = getStorage(getApp(), secondStorageBucket); +connectStorageEmulator(secondStorage, 'localhost', 9199); + +export function StorageTestComponent(): React.JSX.Element { + const [responseData, setResponseData] = useState(''); + + const clearAndSetResponse = (data: string): void => { + setResponseData(''); + setTimeout(() => setResponseData(data), 100); + }; + + const handleUploadString = async (): Promise => { + try { + const secondStorage = getStorage(getApp(), secondStorageBucket); + clearAndSetResponse('Uploading string...'); + const storageReference = ref(secondStorage, 'only-second-bucket/ok.txt'); + const snapshot = await uploadString(storageReference, 'Hello World'); + clearAndSetResponse( + JSON.stringify( + { + success: true, + state: snapshot.state, + metadata: snapshot.metadata, + bytesTransferred: snapshot.bytesTransferred, + totalBytes: snapshot.totalBytes, + }, + null, + 2, + ), + ); + } catch (error: any) { + clearAndSetResponse( + JSON.stringify( + { + success: false, + error: error.message, + code: error.code, + }, + null, + 2, + ), + ); + } + }; + + const handleUploadStringUnauthorized = async (): Promise => { + try { + clearAndSetResponse('Testing unauthorized upload...'); + const storageReference = ref(secondStorage, 'react-native-firebase-testing/should-fail.txt'); + await uploadString(storageReference, 'Hello World'); + clearAndSetResponse( + JSON.stringify( + { + success: false, + error: 'Expected error but upload succeeded', + }, + null, + 2, + ), + ); + } catch (error: any) { + clearAndSetResponse( + JSON.stringify( + { + success: true, + error: error.message, + code: error.code, + expectedError: error.code === 'storage/unauthorized', + }, + null, + 2, + ), + ); + } + }; + + const handleDownloadFile = async (): Promise => { + try { + clearAndSetResponse('Downloading file...'); + const storageRef = ref(storage, 'ok.jpeg'); + const path = `${utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + + await new Promise((resolve, reject) => { + const task = writeToFile(storageRef, path); + const unsubscribe = task.on('state_changed', { + next: snapshot => { + if (snapshot.state === TaskState.SUCCESS) { + clearAndSetResponse( + JSON.stringify( + { + success: true, + state: snapshot.state, + bytesTransferred: snapshot.bytesTransferred, + totalBytes: snapshot.totalBytes, + localPath: path, + }, + null, + 2, + ), + ); + unsubscribe(); + resolve(); + } + }, + error: (error: any) => { + unsubscribe(); + clearAndSetResponse( + JSON.stringify( + { + success: false, + error: error.message, + code: error.code, + }, + null, + 2, + ), + ); + reject(error); + }, + }); + }); + } catch (error: any) { + clearAndSetResponse( + JSON.stringify( + { + success: false, + error: error.message, + code: error.code, + }, + null, + 2, + ), + ); + } + }; + + const handleUploadFile = async (): Promise => { + try { + clearAndSetResponse('Uploading file...'); + const path = `${utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.gif`; + const storageRef = ref(storage, 'uploadOk.jpeg'); + + await new Promise((resolve, reject) => { + const task = putFile(storageRef, path); + const unsubscribe = task.on('state_changed', { + next: snapshot => { + if (snapshot.state === TaskState.SUCCESS) { + clearAndSetResponse( + JSON.stringify( + { + success: true, + state: snapshot.state, + bytesTransferred: snapshot.bytesTransferred, + totalBytes: snapshot.totalBytes, + metadata: snapshot.metadata, + }, + null, + 2, + ), + ); + unsubscribe(); + resolve(); + } + }, + error: (error: any) => { + unsubscribe(); + clearAndSetResponse( + JSON.stringify( + { + success: false, + error: error.message, + code: error.code, + }, + null, + 2, + ), + ); + reject(error); + }, + }); + }); + } catch (error: any) { + clearAndSetResponse( + JSON.stringify( + { + success: false, + error: error.message, + code: error.code, + }, + null, + 2, + ), + ); + } + }; + + const handleUploadStringWithObserver = async (): Promise => { + try { + clearAndSetResponse('Uploading string with observer...'); + const storageRef = ref(storage, 'playground/putStringBlob.json'); + + await new Promise((resolve, reject) => { + const uploadTaskSnapshot = uploadString( + storageRef, + 'Just a string to put in a file for upload', + ); + + const unsubscribe = uploadTaskSnapshot.on('state_changed', { + next: snapshot => { + if (snapshot.state === TaskState.SUCCESS) { + clearAndSetResponse( + JSON.stringify( + { + success: true, + state: snapshot.state, + bytesTransferred: snapshot.bytesTransferred, + totalBytes: snapshot.totalBytes, + metadata: snapshot.metadata, + snapshotAvailable: !!snapshot, + }, + null, + 2, + ), + ); + unsubscribe(); + resolve(); + } + }, + error: (error: any) => { + unsubscribe(); + clearAndSetResponse( + JSON.stringify( + { + success: false, + error: error.message, + code: error.code, + }, + null, + 2, + ), + ); + reject(error); + }, + }); + }); + } catch (error: any) { + clearAndSetResponse( + JSON.stringify( + { + success: false, + error: error.message, + code: error.code, + }, + null, + 2, + ), + ); + } + }; + + return ( + + Storage Test + Ensure Emulator is running!! + + +