From 1f9e7978ed8f6805fad12b32f34b6216ab4556cb Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Thu, 9 Nov 2023 09:50:47 -0800 Subject: [PATCH 01/10] fix broken array-in snippets (#360) * fix broken array-in snippets * npm run snippets * revert array contians anay changes --- firestore-next/test.firestore.js | 2 +- firestore/test.firestore.js | 2 +- snippets/firestore-next/test-firestore/in_filter_with_array.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/firestore-next/test.firestore.js b/firestore-next/test.firestore.js index c961af9d..969c1654 100644 --- a/firestore-next/test.firestore.js +++ b/firestore-next/test.firestore.js @@ -920,7 +920,7 @@ describe("firestore", () => { // [START in_filter_with_array] const { query, where } = require("firebase/firestore"); - const q = query(citiesRef, where('regions', 'in', [['west_coast', 'east_coast']])); + const q = query(citiesRef, where('regions', 'in', [['west_coast'], ['east_coast']])); // [END in_filter_with_array] } }); diff --git a/firestore/test.firestore.js b/firestore/test.firestore.js index ec89172e..20032d36 100644 --- a/firestore/test.firestore.js +++ b/firestore/test.firestore.js @@ -893,7 +893,7 @@ describe("firestore", () => { // [START in_filter_with_array] citiesRef.where('regions', 'in', - [['west_coast', 'east_coast']]); + [['west_coast'], ['east_coast']]); // [END in_filter_with_array] }); diff --git a/snippets/firestore-next/test-firestore/in_filter_with_array.js b/snippets/firestore-next/test-firestore/in_filter_with_array.js index ebe6f18b..190e9dfe 100644 --- a/snippets/firestore-next/test-firestore/in_filter_with_array.js +++ b/snippets/firestore-next/test-firestore/in_filter_with_array.js @@ -7,5 +7,5 @@ // [START in_filter_with_array_modular] import { query, where } from "firebase/firestore"; -const q = query(citiesRef, where('regions', 'in', [['west_coast', 'east_coast']])); +const q = query(citiesRef, where('regions', 'in', [['west_coast'], ['east_coast']])); // [END in_filter_with_array_modular] \ No newline at end of file From e74e8fd9591f70007a44c58580ff0f1fcd1c7959 Mon Sep 17 00:00:00 2001 From: Kevin Sanders Date: Thu, 9 Nov 2023 21:07:49 -0500 Subject: [PATCH 02/10] Typo Fix (#361) --- auth-next/custom-email-handler.js | 4 ++-- .../custom-email-handler/auth_handle_mgmt_query_params.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/auth-next/custom-email-handler.js b/auth-next/custom-email-handler.js index 728baff8..5fcc1f71 100644 --- a/auth-next/custom-email-handler.js +++ b/auth-next/custom-email-handler.js @@ -28,8 +28,8 @@ function handleUserManagementQueryParams() { // Configure the Firebase SDK. // This is the minimum configuration required for the API to be used. const config = { - 'apiKey': "YOU_API_KEY" // Copy this key from the web initialization - // snippet found in the Firebase console. + 'apiKey': "YOUR_API_KEY" // Copy this key from the web initialization + // snippet found in the Firebase console. }; const app = initializeApp(config); const auth = getAuth(app); diff --git a/snippets/auth-next/custom-email-handler/auth_handle_mgmt_query_params.js b/snippets/auth-next/custom-email-handler/auth_handle_mgmt_query_params.js index 6dec3399..d0a22696 100644 --- a/snippets/auth-next/custom-email-handler/auth_handle_mgmt_query_params.js +++ b/snippets/auth-next/custom-email-handler/auth_handle_mgmt_query_params.js @@ -23,8 +23,8 @@ document.addEventListener('DOMContentLoaded', () => { // Configure the Firebase SDK. // This is the minimum configuration required for the API to be used. const config = { - 'apiKey': "YOU_API_KEY" // Copy this key from the web initialization - // snippet found in the Firebase console. + 'apiKey': "YOUR_API_KEY" // Copy this key from the web initialization + // snippet found in the Firebase console. }; const app = initializeApp(config); const auth = getAuth(app); From a9160f62f8ebff02285779974620ad3ff260f5ac Mon Sep 17 00:00:00 2001 From: James Daniels Date: Wed, 13 Mar 2024 16:22:44 -0400 Subject: [PATCH 03/10] FirebaseServerApp (#366) * First. * already snippets for that * cleanup * auth_svc_admin * Auth, not auth-next * Express stub should not be in snippet * Move import into snippet * Update firebaseserverapp.js --- auth/package.json | 1 + auth/service-worker-sessions.js | 43 +++++++++++++++++++++ firebaseserverapp-next/firebaseserverapp.js | 27 +++++++++++++ firebaseserverapp-next/package.json | 12 ++++++ 4 files changed, 83 insertions(+) create mode 100644 firebaseserverapp-next/firebaseserverapp.js create mode 100644 firebaseserverapp-next/package.json diff --git a/auth/package.json b/auth/package.json index a6827027..b4b13f01 100644 --- a/auth/package.json +++ b/auth/package.json @@ -7,6 +7,7 @@ "license": "Apache 2.0", "dependencies": { "firebase": "^8.10.0", + "firebase-admin": "^12.0.0", "firebaseui": "^5.0.0" } } diff --git a/auth/service-worker-sessions.js b/auth/service-worker-sessions.js index babbf91e..05791463 100644 --- a/auth/service-worker-sessions.js +++ b/auth/service-worker-sessions.js @@ -162,3 +162,46 @@ function svcSignInEmail(email, password) { }); // [END auth_svc_sign_in_email] } + +function svcRedirectAdmin() { + const app = { use: (a) => {} }; + + // [START auth_svc_admin] + // Server side code. + const admin = require('firebase-admin'); + + // The Firebase Admin SDK is used here to verify the ID token. + admin.initializeApp(); + + function getIdToken(req) { + // Parse the injected ID token from the request header. + const authorizationHeader = req.headers.authorization || ''; + const components = authorizationHeader.split(' '); + return components.length > 1 ? components[1] : ''; + } + + function checkIfSignedIn(url) { + return (req, res, next) => { + if (req.url == url) { + const idToken = getIdToken(req); + // Verify the ID token using the Firebase Admin SDK. + // User already logged in. Redirect to profile page. + admin.auth().verifyIdToken(idToken).then((decodedClaims) => { + // User is authenticated, user claims can be retrieved from + // decodedClaims. + // In this sample code, authenticated users are always redirected to + // the profile page. + res.redirect('/profile'); + }).catch((error) => { + next(); + }); + } else { + next(); + } + }; + } + + // If a user is signed in, redirect to profile page. + app.use(checkIfSignedIn('/')); + // [END auth_svc_admin] +} diff --git a/firebaseserverapp-next/firebaseserverapp.js b/firebaseserverapp-next/firebaseserverapp.js new file mode 100644 index 00000000..d0ab338c --- /dev/null +++ b/firebaseserverapp-next/firebaseserverapp.js @@ -0,0 +1,27 @@ +// @ts-nocheck +// [START serverapp_auth] +import { initializeServerApp } from 'firebase/app'; +import { getAuth } from 'firebase/auth'; +import { headers } from 'next/headers'; +import { redirect } from 'next/navigation'; + +export default function MyServerComponent() { + + // Get relevant request headers (in Next.JS) + const authIdToken = headers().get('Authorization')?.split('Bearer ')[1]; + + // Initialize the FirebaseServerApp instance. + const serverApp = initializeServerApp(firebaseConfig, { authIdToken }); + + // Initialize Firebase Authentication using the FirebaseServerApp instance. + const auth = getAuth(serverApp); + + if (auth.currentUser) { + redirect('/profile'); + } + + // ... +} +// [END serverapp_auth] + +const firebaseConfig = {}; diff --git a/firebaseserverapp-next/package.json b/firebaseserverapp-next/package.json new file mode 100644 index 00000000..0836b0b2 --- /dev/null +++ b/firebaseserverapp-next/package.json @@ -0,0 +1,12 @@ +{ + "name": "firebaseserverapp-next", + "version": "1.0.0", + "scripts": { + "compile": "cp ../tsconfig.json.template ./tsconfig.json && tsc" + }, + "license": "Apache-2.0", + "dependencies": { + "firebase": "^10.0.0", + "next": "^14.1.3" + } +} From 1abb6ce1a784ae5552946dff5f1f5aab7dcbda30 Mon Sep 17 00:00:00 2001 From: "Nhien (Ricky) Lam" <62775270+NhienLam@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:24:45 -0700 Subject: [PATCH 04/10] =?UTF-8?q?Add=20snippets=20for=20Handling=20the=20a?= =?UTF-8?q?ccount-exists-with-different=E2=80=A6=20(#367)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add v8 and v9 snippets for Handling the account-exists-with-different-credential error * Import auth functions * Add missing methods to param * Add missing methods to params for legacy snippets --- auth-next/link-multiple-accounts.js | 58 ++++++++++++++++++ auth/link-multiple-accounts.js | 56 +++++++++++++++++ .../account_exists_popup.js | 61 +++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 snippets/auth-next/link-multiple-accounts/account_exists_popup.js diff --git a/auth-next/link-multiple-accounts.js b/auth-next/link-multiple-accounts.js index 34c0db58..1938aafc 100644 --- a/auth-next/link-multiple-accounts.js +++ b/auth-next/link-multiple-accounts.js @@ -182,3 +182,61 @@ function unlink(providerId) { }); // [END auth_unlink_provider] } + +function accountExistsPopup(auth, facebookProvider, goToApp, promptUserForPassword, promptUserForSignInMethod, getProviderForProviderId) { + // [START account_exists_popup] + const { signInWithPopup, signInWithEmailAndPassword, linkWithCredential } = require("firebase/auth"); + + // User tries to sign in with Facebook. + signInWithPopup(auth, facebookProvider).catch((error) => { + // User's email already exists. + if (error.code === 'auth/account-exists-with-different-credential') { + // The pending Facebook credential. + const pendingCred = error.credential; + // The provider account's email address. + const email = error.customData.email; + + // Present the user with a list of providers they might have + // used to create the original account. + // Then, ask the user to sign in with the existing provider. + const method = promptUserForSignInMethod(); + + if (method === 'password') { + // TODO: Ask the user for their password. + // In real scenario, you should handle this asynchronously. + const password = promptUserForPassword(); + signInWithEmailAndPassword(auth, email, password).then((result) => { + return linkWithCredential(result.user, pendingCred); + }).then(() => { + // Facebook account successfully linked to the existing user. + goToApp(); + }); + return; + } + + // All other cases are external providers. + // Construct provider object for that provider. + // TODO: Implement getProviderForProviderId. + const provider = getProviderForProviderId(method); + // At this point, you should let the user know that they already have an + // account with a different provider, and validate they want to sign in + // with the new provider. + // Note: Browsers usually block popups triggered asynchronously, so in + // real app, you should ask the user to click on a "Continue" button + // that will trigger signInWithPopup(). + signInWithPopup(auth, provider).then((result) => { + // Note: Identity Platform doesn't control the provider's sign-in + // flow, so it's possible for the user to sign in with an account + // with a different email from the first one. + + // Link the Facebook credential. We have access to the pending + // credential, so we can directly call the link method. + linkWithCredential(result.user, pendingCred).then((userCred) => { + // Success. + goToApp(); + }); + }); + } +}); +// [END account_exists_popup] +} diff --git a/auth/link-multiple-accounts.js b/auth/link-multiple-accounts.js index 30f5a2ec..8eed77b8 100644 --- a/auth/link-multiple-accounts.js +++ b/auth/link-multiple-accounts.js @@ -166,3 +166,59 @@ function unlink(providerId) { }); // [END auth_unlink_provider] } + +function accountExistsPopup(facebookProvider, goToApp, promptUserForPassword, promptUserForSignInMethod, getProviderForProviderId) { + // [START account_exists_popup] + // User tries to sign in with Facebook. + auth.signInWithPopup(facebookProvider).catch((error) => { + // User's email already exists. + if (error.code === 'auth/account-exists-with-different-credential') { + // The pending Facebook credential. + const pendingCred = error.credential; + // The provider account's email address. + const email = error.email; + + // Present the user with a list of providers they might have + // used to create the original account. + // Then, ask the user to sign in with the existing provider. + const method = promptUserForSignInMethod(); + + if (method === 'password') { + // TODO: Ask the user for their password. + // In real scenario, you should handle this asynchronously. + const password = promptUserForPassword(); + auth.signInWithEmailAndPassword(email, password).then((result) => { + return result.user.linkWithCredential(pendingCred); + }).then(() => { + // Facebook account successfully linked to the existing user. + goToApp(); + }); + return; + } + + // All other cases are external providers. + // Construct provider object for that provider. + // TODO: Implement getProviderForProviderId. + const provider = getProviderForProviderId(method); + // At this point, you should let the user know that they already have an + // account with a different provider, and validate they want to sign in + // with the new provider. + // Note: Browsers usually block popups triggered asynchronously, so in + // real app, you should ask the user to click on a "Continue" button + // that will trigger signInWithPopup(). + auth.signInWithPopup(provider).then((result) => { + // Note: Identity Platform doesn't control the provider's sign-in + // flow, so it's possible for the user to sign in with an account + // with a different email from the first one. + + // Link the Facebook credential. We have access to the pending + // credential, so we can directly call the link method. + result.user.linkWithCredential(pendingCred).then((userCred) => { + // Success. + goToApp(); + }); + }); + } +}); +// [END account_exists_popup] +} diff --git a/snippets/auth-next/link-multiple-accounts/account_exists_popup.js b/snippets/auth-next/link-multiple-accounts/account_exists_popup.js new file mode 100644 index 00000000..2c667882 --- /dev/null +++ b/snippets/auth-next/link-multiple-accounts/account_exists_popup.js @@ -0,0 +1,61 @@ +// This snippet file was generated by processing the source file: +// ./auth-next/link-multiple-accounts.js +// +// To update the snippets in this file, edit the source and then run +// 'npm run snippets'. + + // [START account_exists_popup_modular] + import { signInWithPopup, signInWithEmailAndPassword, linkWithCredential } from "firebase/auth"; + + // User tries to sign in with Facebook. + signInWithPopup(auth, facebookProvider).catch((error) => { + // User's email already exists. + if (error.code === 'auth/account-exists-with-different-credential') { + // The pending Facebook credential. + const pendingCred = error.credential; + // The provider account's email address. + const email = error.customData.email; + + // Present the user with a list of providers they might have + // used to create the original account. + // Then, ask the user to sign in with the existing provider. + const method = promptUserForSignInMethod(); + + if (method === 'password') { + // TODO: Ask the user for their password. + // In real scenario, you should handle this asynchronously. + const password = promptUserForPassword(); + signInWithEmailAndPassword(auth, email, password).then((result) => { + return linkWithCredential(result.user, pendingCred); + }).then(() => { + // Facebook account successfully linked to the existing user. + goToApp(); + }); + return; + } + + // All other cases are external providers. + // Construct provider object for that provider. + // TODO: Implement getProviderForProviderId. + const provider = getProviderForProviderId(method); + // At this point, you should let the user know that they already have an + // account with a different provider, and validate they want to sign in + // with the new provider. + // Note: Browsers usually block popups triggered asynchronously, so in + // real app, you should ask the user to click on a "Continue" button + // that will trigger signInWithPopup(). + signInWithPopup(auth, provider).then((result) => { + // Note: Identity Platform doesn't control the provider's sign-in + // flow, so it's possible for the user to sign in with an account + // with a different email from the first one. + + // Link the Facebook credential. We have access to the pending + // credential, so we can directly call the link method. + linkWithCredential(result.user, pendingCred).then((userCred) => { + // Success. + goToApp(); + }); + }); + } +}); +// [END account_exists_popup_modular] \ No newline at end of file From 2117c619ce49b08d582f282e7d2ec798f5105c3d Mon Sep 17 00:00:00 2001 From: spider-hand Date: Wed, 29 May 2024 01:05:44 +0900 Subject: [PATCH 05/10] chore: update outdated comments (#370) --- auth-next/email-link-auth.js | 10 ++++++---- .../auth-next/email-link-auth/email_link_complete.js | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/auth-next/email-link-auth.js b/auth-next/email-link-auth.js index 59ea2115..4748e699 100644 --- a/auth-next/email-link-auth.js +++ b/auth-next/email-link-auth.js @@ -68,11 +68,13 @@ function emailLinkComplete() { .then((result) => { // Clear email from storage. window.localStorage.removeItem('emailForSignIn'); - // You can access the new user via result.user - // Additional user info profile not available via: - // result.additionalUserInfo.profile == null + // You can access the new user by importing getAdditionalUserInfo + // and calling it with result: + // getAdditionalUserInfo(result) + // You can access the user's profile via: + // getAdditionalUserInfo(result)?.profile // You can check if the user is new or existing: - // result.additionalUserInfo.isNewUser + // getAdditionalUserInfo(result)?.isNewUser }) .catch((error) => { // Some error occurred, you can inspect the code: error.code diff --git a/snippets/auth-next/email-link-auth/email_link_complete.js b/snippets/auth-next/email-link-auth/email_link_complete.js index b42b9af9..dbe1143d 100644 --- a/snippets/auth-next/email-link-auth/email_link_complete.js +++ b/snippets/auth-next/email-link-auth/email_link_complete.js @@ -26,11 +26,13 @@ if (isSignInWithEmailLink(auth, window.location.href)) { .then((result) => { // Clear email from storage. window.localStorage.removeItem('emailForSignIn'); - // You can access the new user via result.user - // Additional user info profile not available via: - // result.additionalUserInfo.profile == null + // You can access the new user by importing getAdditionalUserInfo + // and calling it with result: + // getAdditionalUserInfo(result) + // You can access the user's profile via: + // getAdditionalUserInfo(result)?.profile // You can check if the user is new or existing: - // result.additionalUserInfo.isNewUser + // getAdditionalUserInfo(result)?.isNewUser }) .catch((error) => { // Some error occurred, you can inspect the code: error.code From 56d70627e2dc275f01cd0e55699794bf40faca80 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 8 Oct 2024 15:17:01 -0400 Subject: [PATCH 06/10] Upgrade versions in FCM service worker (#382) * Upgrade versions in FCM service worker * Add comment telling users to use latest --- messaging/service-worker.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/messaging/service-worker.js b/messaging/service-worker.js index c0f875bb..1a202159 100644 --- a/messaging/service-worker.js +++ b/messaging/service-worker.js @@ -10,8 +10,9 @@ function initInSw() { // Give the service worker access to Firebase Messaging. // Note that you can only use Firebase Messaging here. Other Firebase libraries // are not available in the service worker. - importScripts('https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js'); - importScripts('https://www.gstatic.com/firebasejs/8.10.1/firebase-messaging.js'); + // Replace 10.13.2 with latest version of the Firebase JS SDK. + importScripts('https://www.gstatic.com/firebasejs/10.13.2/firebase-app-compat.js'); + importScripts('https://www.gstatic.com/firebasejs/10.13.2/firebase-messaging-compat.js'); // Initialize the Firebase app in the service worker by passing in // your app's Firebase config object. From 3d04522869ad402c4da3ae74d6b30a4dbf8d1233 Mon Sep 17 00:00:00 2001 From: Athira M Date: Fri, 24 Jan 2025 12:17:10 +0530 Subject: [PATCH 07/10] RC doc update to provide default prod value for minimumFetchIntervalMillis (#389) * Add comment to highlight the default prod value for minimumFetchIntervalMillis. Issue addressed: https://github.com/firebase/firebase-js-sdk/issues/2841 * Update code snipped with default prod value for minimumFetchIntervalMillis. * Update code snipped with default prod value for minimumFetchIntervalMillis. * Update code snipped with default prod value for minimumFetchIntervalMillis. --------- Co-authored-by: Athira M --- remoteconfig-next/index.js | 1 + snippets/remoteconfig-next/index/rc_set_minimum_fetch_time.js | 1 + 2 files changed, 2 insertions(+) diff --git a/remoteconfig-next/index.js b/remoteconfig-next/index.js index aeeb263a..287f3c99 100644 --- a/remoteconfig-next/index.js +++ b/remoteconfig-next/index.js @@ -14,6 +14,7 @@ function getInstance() { function setMinimumFetchTime() { const remoteConfig = getInstance(); // [START rc_set_minimum_fetch_time] + // The default and recommended production fetch interval for Remote Config is 12 hours remoteConfig.settings.minimumFetchIntervalMillis = 3600000; // [END rc_set_minimum_fetch_time] } diff --git a/snippets/remoteconfig-next/index/rc_set_minimum_fetch_time.js b/snippets/remoteconfig-next/index/rc_set_minimum_fetch_time.js index 131976c5..77aa9f45 100644 --- a/snippets/remoteconfig-next/index/rc_set_minimum_fetch_time.js +++ b/snippets/remoteconfig-next/index/rc_set_minimum_fetch_time.js @@ -5,5 +5,6 @@ // 'npm run snippets'. // [START rc_set_minimum_fetch_time_modular] +// The default and recommended production fetch interval for Remote Config is 12 hours remoteConfig.settings.minimumFetchIntervalMillis = 3600000; // [END rc_set_minimum_fetch_time_modular] \ No newline at end of file From 467eaa165dcbd9b3ab15711e76fa52237ba37f8b Mon Sep 17 00:00:00 2001 From: "Nhien (Ricky) Lam" <62775270+NhienLam@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:39:06 -0800 Subject: [PATCH 08/10] Update email link auth snippet to stop using deprecated `dynamicLinkDomain` (#390) * Update email link auth snippet to remove FDL * Update custom domain example --- auth-next/email-link-auth.js | 3 ++- .../email-link-auth/auth_email_link_actioncode_settings.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/auth-next/email-link-auth.js b/auth-next/email-link-auth.js index 4748e699..fde67663 100644 --- a/auth-next/email-link-auth.js +++ b/auth-next/email-link-auth.js @@ -19,7 +19,8 @@ function emailLinkActionCodeSettings() { installApp: true, minimumVersion: '12' }, - dynamicLinkDomain: 'example.page.link' + // The domain must be configured in Firebase Hosting and owned by the project. + linkDomain: 'custom-domain.com' }; // [END auth_email_link_actioncode_settings] } diff --git a/snippets/auth-next/email-link-auth/auth_email_link_actioncode_settings.js b/snippets/auth-next/email-link-auth/auth_email_link_actioncode_settings.js index a1480a11..010b5c09 100644 --- a/snippets/auth-next/email-link-auth/auth_email_link_actioncode_settings.js +++ b/snippets/auth-next/email-link-auth/auth_email_link_actioncode_settings.js @@ -19,6 +19,7 @@ const actionCodeSettings = { installApp: true, minimumVersion: '12' }, - dynamicLinkDomain: 'example.page.link' + // The domain must be configured in Firebase Hosting and owned by the project. + linkDomain: 'custom-domain.com' }; // [END auth_email_link_actioncode_settings_modular] \ No newline at end of file From 89e03fd80969f2f237888d92d3291303523dbfdf Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Tue, 28 Oct 2025 13:54:41 -0700 Subject: [PATCH 09/10] Add OR query snippets (#326) * add simpler snippet * consts * add OR snippets * run snippets * fix web snippet * fix broken array-in snippets * fix old actions deps * update dev deps * update firestore deps * use node 20 and 22 * try hoisting * verbose log * downgrade rimraf * remove pnpm * fix build, grossly * migrate to new eslint config * add missing installations compile * add compile to scripts * skip scripts * fix and run snippets * add 22.x back --- .eslintrc.json | 20 ----- .github/workflows/test.yml | 9 ++- .gitignore | 1 - eslint.config.js | 14 ++++ firestore-next/package.json | 12 +-- firestore-next/test.firestore.js | 74 ++++++++++++++++++- firestore-next/test.solution-geoqueries.js | 5 ++ firestore/test.firestore.js | 2 +- installations/package.json | 4 +- package.json | 19 ++--- scripts/checkdirty.sh | 2 +- scripts/compile.sh | 33 +++++++++ scripts/package.json | 1 + .../array_contains_any_filter.js | 2 +- .../test-firestore/four_disjunctions.js | 15 ++++ .../four_disjunctions_compact.js | 13 ++++ .../test-firestore/one_disjunction.js | 9 +++ .../firestore-next/test-firestore/or_query.js | 15 ++++ .../test-firestore/two_disjunctions.js | 9 +++ .../fs_geo_query_hashes.js | 2 + 20 files changed, 216 insertions(+), 45 deletions(-) delete mode 100644 .eslintrc.json create mode 100644 eslint.config.js create mode 100755 scripts/compile.sh create mode 100644 snippets/firestore-next/test-firestore/four_disjunctions.js create mode 100644 snippets/firestore-next/test-firestore/four_disjunctions_compact.js create mode 100644 snippets/firestore-next/test-firestore/one_disjunction.js create mode 100644 snippets/firestore-next/test-firestore/or_query.js create mode 100644 snippets/firestore-next/test-firestore/two_disjunctions.js diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 7e4337ee..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "env": { - "browser": true, - "commonjs": true, - "es2021": true - }, - "parserOptions": { - "ecmaVersion": 12, - "sourceType": "module" - }, - "rules": { - "arrow-parens": ["error", "always"], - "arrow-spacing": ["error"], - "no-const-assign": ["error"], - "prefer-const": ["error"], - "prefer-arrow-callback": ["error"], - "spaced-comment": ["error", "always"], - "semi": ["error", "always"] - } -} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ee2c87f1..50cd5f0b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,15 +13,16 @@ jobs: strategy: matrix: node-version: - - 14.x + - 20.x + - 22.x steps: - - uses: actions/checkout@v1 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v5 + - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Cache npm - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package.json') }} diff --git a/.gitignore b/.gitignore index 435f90ea..9d79b150 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ tsconfig.json dist/ .DS_Store .idea -pnpm-lock.yaml diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..4e750c2f --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,14 @@ + +import { defineConfig } from "eslint/config"; +import js from "@eslint/js"; + +export default defineConfig([ + js.configs.recommended, + { + rules: { + "no-unused-vars": "off", + "no-undef": "off", + "no-redeclare": "off" + }, + }, +]); diff --git a/firestore-next/package.json b/firestore-next/package.json index ef3b8708..faff0996 100644 --- a/firestore-next/package.json +++ b/firestore-next/package.json @@ -6,13 +6,13 @@ }, "license": "Apache-2.0", "dependencies": { - "firebase": "^10.0.0", - "geofire-common": "^5.1.0" + "firebase": "^12.4.0", + "geofire-common": "^6.0.0" }, "devDependencies": { - "@types/chai": "^4.2.12", - "@types/mocha": "^8.0.3", - "chai": "^4.2.0", - "mocha": "^8.1.3" + "@types/chai": "^5.2.3", + "@types/mocha": "^10.0.10", + "chai": "^6.2.0", + "mocha": "^11.7.4" } } diff --git a/firestore-next/test.firestore.js b/firestore-next/test.firestore.js index 969c1654..214077fd 100644 --- a/firestore-next/test.firestore.js +++ b/firestore-next/test.firestore.js @@ -2,6 +2,7 @@ // [SNIPPETS_SEPARATION enabled] const { expect } = require('chai'); +import { or } from "firebase/firestore"; // [START city_custom_object] class City { @@ -892,7 +893,7 @@ describe("firestore", () => { const { query, where } = require("firebase/firestore"); const q = query(citiesRef, - where('regions', 'array-contains-any', ['west_coast', 'east_coast'])); + where('regions', 'array-contains-any', [['west_coast'], ['east_coast']])); // [END array_contains_any_filter] }); @@ -1102,6 +1103,77 @@ describe("firestore", () => { limit(25)); // [END paginate] }); + + it("should handle OR queries", async () => { + const { collection, query, where, and } = require("firebase/firestore"); + // [START or_query] + const q = query(collection(db, "cities"), and( + where('state', '==', 'CA'), + or( + where('capital', '==', true), + where('population', '>=', 1000000) + ) + )); + // [END or_query] + }); + + it("should allow for 30 or fewer disjunctions", async () => { + const { collection, query, where, and } = require("firebase/firestore"); + const collectionRef = collection(db, "cities"); + // [START one_disjunction] + query(collectionRef, where("a", "==", 1)); + // [END one_disjunction] + + // [START two_disjunctions] + query(collectionRef, or( where("a", "==", 1), where("b", "==", 2) )); + // [END two_disjunctions] + + // [START four_disjunctions] + query(collectionRef, + or( and( where("a", "==", 1), where("c", "==", 3) ), + and( where("a", "==", 1), where("d", "==", 4) ), + and( where("b", "==", 2), where("c", "==", 3) ), + and( where("b", "==", 2), where("d", "==", 4) ) + ) + ); + // [END four_disjunctions] + + // [START four_disjunctions_compact] + query(collectionRef, + and( or( where("a", "==", 1), where("b", "==", 2) ), + or( where("c", "==", 3), where("d", "==", 4) ) + ) + ); + // [END four_disjunctions_compact] + + expect(() => { + // [START 50_disjunctions] + query(collectionRef, + and( where("a", "in", [1, 2, 3, 4, 5]), + where("b", "in", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ) + ); + // [END 50_disjunctions] + }).to.throw; + + // [START 20_disjunctions] + query(collectionRef, + or( where("a", "in", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + where("b", "in", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ) + ); + // [END 20_disjunctions] + + // [START 10_disjunctions] + query(collectionRef, + and( where("a", "in", [1, 2, 3, 4, 5]), + or( where("b", "==", 2), + where("c", "==", 3) + ) + ) + ); + // [END 10_disjunctions] + }); }); describe('collectionGroup(landmarks)', () => { diff --git a/firestore-next/test.solution-geoqueries.js b/firestore-next/test.solution-geoqueries.js index 507c6178..eab7aa61 100644 --- a/firestore-next/test.solution-geoqueries.js +++ b/firestore-next/test.solution-geoqueries.js @@ -29,6 +29,9 @@ async function addHash(done) { done(); } +// tsc complains `center` can have more or fewer than 2 elements, but +// since this is a js file there's no way of more accurately specifying +// `center`'s type. async function queryHashes(done) { // [START fs_geo_query_hashes] const { collection, query, orderBy, startAt, endAt, getDocs } = require('firebase/firestore'); @@ -40,6 +43,7 @@ async function queryHashes(done) { // Each item in 'bounds' represents a startAt/endAt pair. We have to issue // a separate query for each pair. There can be up to 9 pairs of bounds // depending on overlap, but in most cases there are 4. + // @ts-ignore const bounds = geofire.geohashQueryBounds(center, radiusInM); const promises = []; for (const b of bounds) { @@ -63,6 +67,7 @@ async function queryHashes(done) { // We have to filter out a few false positives due to GeoHash // accuracy, but most will match + // @ts-ignore const distanceInKm = geofire.distanceBetween([lat, lng], center); const distanceInM = distanceInKm * 1000; if (distanceInM <= radiusInM) { diff --git a/firestore/test.firestore.js b/firestore/test.firestore.js index 20032d36..7d820f0b 100644 --- a/firestore/test.firestore.js +++ b/firestore/test.firestore.js @@ -877,7 +877,7 @@ describe("firestore", () => { const citiesRef = db.collection('cities'); // [START array_contains_any_filter] citiesRef.where('regions', 'array-contains-any', - ['west_coast', 'east_coast']); + [['west_coast'], ['east_coast']]); // [END array_contains_any_filter] }); diff --git a/installations/package.json b/installations/package.json index 1395fca3..b201b2bb 100644 --- a/installations/package.json +++ b/installations/package.json @@ -1,7 +1,9 @@ { "name": "installations", "version": "1.0.0", - "scripts": {}, + "scripts": { + "compile": "echo 'Installations build temporarily disabled'" + }, "license": "Apache 2.0", "dependencies": { "firebase": "^8.10.0" diff --git a/package.json b/package.json index cb7d9157..16ceeeb0 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,21 @@ { "name": "snippets-web", "version": "1.0.0", + "type": "module", "scripts": { - "snippets": "rimraf snippets && ts-node scripts/separate-snippets.ts", + "snippets": "rimraf snippets && node --loader ts-node/esm scripts/separate-snippets.ts", "lint": "git ls-files | grep -v 'snippets/' | grep '.js$' | xargs npx eslint", "format": "npm run lint -- --fix", - "bootstrap": "pnpm recursive install", - "compile": "pnpm recursive run compile --workspace-concurrency=4" + "bootstrap": "find . -type f -name package.json -not -path '*/node_modules/*' -exec bash -c 'cd $(dirname {}) && npm install' \\;", + "compile": "bash scripts/compile.sh" }, "license": "Apache-2.0", "devDependencies": { - "@types/node": "^16.7.10", - "eslint": "^7.32.0", - "pnpm": "^6.14.6", - "rimraf": "^3.0.2", - "ts-node": "^10.2.1", - "typescript": "^4.4.2" + "@eslint/js": "^9.38.0", + "@types/node": "^24.9.1", + "eslint": "^9.38.0", + "rimraf": "^5.0.10", + "ts-node": "^10.9.2", + "typescript": "^5.9.0" } } diff --git a/scripts/checkdirty.sh b/scripts/checkdirty.sh index 5d3c8bea..ccf1aea6 100755 --- a/scripts/checkdirty.sh +++ b/scripts/checkdirty.sh @@ -11,5 +11,5 @@ if [[ $(git diff --stat HEAD) != '' ]]; then echo 'Error: git diff is dirty ... did you forget to run "npm run snippets" after adding snippets?' exit 1 else - echo 'Succes: git diff is clean' + echo 'Success: git diff is clean' fi diff --git a/scripts/compile.sh b/scripts/compile.sh new file mode 100755 index 00000000..f0e00782 --- /dev/null +++ b/scripts/compile.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Runs `npm run compile` across all snippet directories. + +echo "Starting package compilation search..." + +# Iterate over all direct subdirectories (ending with /) +# The glob pattern '*/' ensures we only look at directories at this level. +for dir in */; do + # Strip the trailing slash for cleaner output and directory path usage + subdir="${dir%/}" + + # Check if a package.json file exists in the current subdirectory + if [[ -f "$subdir/package.json" && "$subdir" != "scripts" ]]; then + echo "--> Found package.json in '$subdir'. Running 'npm run compile'..." + + # Use a subshell (parentheses) for the 'cd' command. + # This executes 'cd' and 'npm run compile' in a separate process, + # ensuring the main script's working directory doesn't change, + # and the loop continues correctly from the parent path. + if (cd "$subdir" && npm run compile); then + echo "Successfully compiled '$subdir'." + else + echo "ERROR: Compilation failed for '$subdir'. Check the output above." + exit 1 + fi + + else + : # skip directories like snippets and .github + fi +done + +echo "Package compilation search complete." diff --git a/scripts/package.json b/scripts/package.json index 72b9bf11..8880dade 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -1,6 +1,7 @@ { "name": "scripts", "version": "1.0.0", + "type": "module", "description": "Internal repo scripts", "scripts": { }, diff --git a/snippets/firestore-next/test-firestore/array_contains_any_filter.js b/snippets/firestore-next/test-firestore/array_contains_any_filter.js index 3ee6e4d9..c263ec7b 100644 --- a/snippets/firestore-next/test-firestore/array_contains_any_filter.js +++ b/snippets/firestore-next/test-firestore/array_contains_any_filter.js @@ -8,5 +8,5 @@ import { query, where } from "firebase/firestore"; const q = query(citiesRef, - where('regions', 'array-contains-any', ['west_coast', 'east_coast'])); + where('regions', 'array-contains-any', [['west_coast'], ['east_coast']])); // [END array_contains_any_filter_modular] \ No newline at end of file diff --git a/snippets/firestore-next/test-firestore/four_disjunctions.js b/snippets/firestore-next/test-firestore/four_disjunctions.js new file mode 100644 index 00000000..12ed21ee --- /dev/null +++ b/snippets/firestore-next/test-firestore/four_disjunctions.js @@ -0,0 +1,15 @@ +// This snippet file was generated by processing the source file: +// ./firestore-next/test.firestore.js +// +// To update the snippets in this file, edit the source and then run +// 'npm run snippets'. + +// [START four_disjunctions_modular] +query(collectionRef, + or( and( where("a", "==", 1), where("c", "==", 3) ), + and( where("a", "==", 1), where("d", "==", 4) ), + and( where("b", "==", 2), where("c", "==", 3) ), + and( where("b", "==", 2), where("d", "==", 4) ) + ) +); +// [END four_disjunctions_modular] \ No newline at end of file diff --git a/snippets/firestore-next/test-firestore/four_disjunctions_compact.js b/snippets/firestore-next/test-firestore/four_disjunctions_compact.js new file mode 100644 index 00000000..7421b01a --- /dev/null +++ b/snippets/firestore-next/test-firestore/four_disjunctions_compact.js @@ -0,0 +1,13 @@ +// This snippet file was generated by processing the source file: +// ./firestore-next/test.firestore.js +// +// To update the snippets in this file, edit the source and then run +// 'npm run snippets'. + +// [START four_disjunctions_compact_modular] +query(collectionRef, + and( or( where("a", "==", 1), where("b", "==", 2) ), + or( where("c", "==", 3), where("d", "==", 4) ) + ) +); +// [END four_disjunctions_compact_modular] \ No newline at end of file diff --git a/snippets/firestore-next/test-firestore/one_disjunction.js b/snippets/firestore-next/test-firestore/one_disjunction.js new file mode 100644 index 00000000..f2a5ebbe --- /dev/null +++ b/snippets/firestore-next/test-firestore/one_disjunction.js @@ -0,0 +1,9 @@ +// This snippet file was generated by processing the source file: +// ./firestore-next/test.firestore.js +// +// To update the snippets in this file, edit the source and then run +// 'npm run snippets'. + +// [START one_disjunction_modular] +query(collectionRef, where("a", "==", 1)); +// [END one_disjunction_modular] \ No newline at end of file diff --git a/snippets/firestore-next/test-firestore/or_query.js b/snippets/firestore-next/test-firestore/or_query.js new file mode 100644 index 00000000..0b419b37 --- /dev/null +++ b/snippets/firestore-next/test-firestore/or_query.js @@ -0,0 +1,15 @@ +// This snippet file was generated by processing the source file: +// ./firestore-next/test.firestore.js +// +// To update the snippets in this file, edit the source and then run +// 'npm run snippets'. + +// [START or_query_modular] +const q = query(collection(db, "cities"), and( + where('state', '==', 'CA'), + or( + where('capital', '==', true), + where('population', '>=', 1000000) + ) +)); +// [END or_query_modular] \ No newline at end of file diff --git a/snippets/firestore-next/test-firestore/two_disjunctions.js b/snippets/firestore-next/test-firestore/two_disjunctions.js new file mode 100644 index 00000000..e9015fc3 --- /dev/null +++ b/snippets/firestore-next/test-firestore/two_disjunctions.js @@ -0,0 +1,9 @@ +// This snippet file was generated by processing the source file: +// ./firestore-next/test.firestore.js +// +// To update the snippets in this file, edit the source and then run +// 'npm run snippets'. + +// [START two_disjunctions_modular] +query(collectionRef, or( where("a", "==", 1), where("b", "==", 2) )); +// [END two_disjunctions_modular] \ No newline at end of file diff --git a/snippets/firestore-next/test-solution-geoqueries/fs_geo_query_hashes.js b/snippets/firestore-next/test-solution-geoqueries/fs_geo_query_hashes.js index 95a8863c..213cb77e 100644 --- a/snippets/firestore-next/test-solution-geoqueries/fs_geo_query_hashes.js +++ b/snippets/firestore-next/test-solution-geoqueries/fs_geo_query_hashes.js @@ -14,6 +14,7 @@ const radiusInM = 50 * 1000; // Each item in 'bounds' represents a startAt/endAt pair. We have to issue // a separate query for each pair. There can be up to 9 pairs of bounds // depending on overlap, but in most cases there are 4. +// @ts-ignore const bounds = geofire.geohashQueryBounds(center, radiusInM); const promises = []; for (const b of bounds) { @@ -37,6 +38,7 @@ for (const snap of snapshots) { // We have to filter out a few false positives due to GeoHash // accuracy, but most will match + // @ts-ignore const distanceInKm = geofire.distanceBetween([lat, lng], center); const distanceInM = distanceInKm * 1000; if (distanceInM <= radiusInM) { From 95c8c159ff4d90af442352f058406f1aeb8adcbb Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Tue, 28 Oct 2025 14:09:59 -0700 Subject: [PATCH 10/10] Add new web persistence (#331) * Add new web persistence * code review changes * fix snippet * run snippets --- firestore-next/test.firestore.js | 40 +++++++++++++------ .../test-firestore/initialize_persistence.js | 32 +++++++++------ 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/firestore-next/test.firestore.js b/firestore-next/test.firestore.js index 214077fd..0300fb6f 100644 --- a/firestore-next/test.firestore.js +++ b/firestore-next/test.firestore.js @@ -74,22 +74,36 @@ describe("firestore", () => { const db = getFirestore(app); + const { + initializeFirestore, + memoryLocalCache, + persistentLocalCache, + persistentSingleTabManager, + persistentMultipleTabManager + } = require("firebase/firestore"); + // [START initialize_persistence] - const { enableIndexedDbPersistence } = require("firebase/firestore"); + // Memory cache is the default if no config is specified. + initializeFirestore(app, {}); - enableIndexedDbPersistence(db) - .catch((err) => { - if (err.code == 'failed-precondition') { - // Multiple tabs open, persistence can only be enabled - // in one tab at a a time. - // ... - } else if (err.code == 'unimplemented') { - // The current browser does not support all of the - // features required to enable persistence - // ... - } + // This is the default behavior if no persistence is specified. + initializeFirestore(app, {localCache: memoryLocalCache()}); + + // Defaults to single-tab persistence if no tab manager is specified. + initializeFirestore(app, {localCache: persistentLocalCache(/*settings*/{})}); + + // Same as `initializeFirestore(app, {localCache: persistentLocalCache(/*settings*/{})})`, + // but more explicit about tab management. + initializeFirestore(app, + {localCache: + persistentLocalCache(/*settings*/{tabManager: persistentSingleTabManager({})}) + }); + + // Use multi-tab IndexedDb persistence. + initializeFirestore(app, + {localCache: + persistentLocalCache(/*settings*/{tabManager: persistentMultipleTabManager()}) }); - // Subsequent queries will use persistence, if it was enabled successfully // [END initialize_persistence] }); diff --git a/snippets/firestore-next/test-firestore/initialize_persistence.js b/snippets/firestore-next/test-firestore/initialize_persistence.js index acd5e988..c08d8825 100644 --- a/snippets/firestore-next/test-firestore/initialize_persistence.js +++ b/snippets/firestore-next/test-firestore/initialize_persistence.js @@ -5,19 +5,25 @@ // 'npm run snippets'. // [START initialize_persistence_modular] -import { enableIndexedDbPersistence } from "firebase/firestore"; +// Memory cache is the default if no config is specified. +initializeFirestore(app, {}); -enableIndexedDbPersistence(db) - .catch((err) => { - if (err.code == 'failed-precondition') { - // Multiple tabs open, persistence can only be enabled - // in one tab at a a time. - // ... - } else if (err.code == 'unimplemented') { - // The current browser does not support all of the - // features required to enable persistence - // ... - } +// This is the default behavior if no persistence is specified. +initializeFirestore(app, {localCache: memoryLocalCache()}); + +// Defaults to single-tab persistence if no tab manager is specified. +initializeFirestore(app, {localCache: persistentLocalCache(/*settings*/{})}); + +// Same as `initializeFirestore(app, {localCache: persistentLocalCache(/*settings*/{})})`, +// but more explicit about tab management. +initializeFirestore(app, + {localCache: + persistentLocalCache(/*settings*/{tabManager: persistentSingleTabManager({})}) +}); + +// Use multi-tab IndexedDb persistence. +initializeFirestore(app, + {localCache: + persistentLocalCache(/*settings*/{tabManager: persistentMultipleTabManager()}) }); -// Subsequent queries will use persistence, if it was enabled successfully // [END initialize_persistence_modular] \ No newline at end of file