From 1890f07e9965935597e7fc7023c197a5191815e1 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Wed, 7 Jan 2026 15:02:25 -0800 Subject: [PATCH 1/9] First draft of firestore-basics skill --- skills/firestore-basics/SKILL.md | 20 +++++ .../references/provisioning.md | 38 +++++++++ .../firestore-basics/references/sdk_usage.md | 77 +++++++++++++++++++ .../references/security_rules.md | 58 ++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 skills/firestore-basics/SKILL.md create mode 100644 skills/firestore-basics/references/provisioning.md create mode 100644 skills/firestore-basics/references/sdk_usage.md create mode 100644 skills/firestore-basics/references/security_rules.md diff --git a/skills/firestore-basics/SKILL.md b/skills/firestore-basics/SKILL.md new file mode 100644 index 00000000000..73c166cf35f --- /dev/null +++ b/skills/firestore-basics/SKILL.md @@ -0,0 +1,20 @@ +--- +name: firestore-basics +description: Comprehensive guide for Firestore basics including provisioning, security rules, and SDK usage. Use this skill when the user needs help setting up Firestore, writing security rules, or using the Firestore SDK in their application. +--- + +# Firestore Basics + +This skill provides a complete guide for getting started with Cloud Firestore, including provisioning, securing, and integrating it into your application. + +## Provisioning + +To set up Cloud Firestore in your Firebase project and local environment, see [provisioning.md](references/provisioning.md). + +## Security Rules + +For guidance on writing and deploying Firestore Security Rules to protect your data, see [security_rules.md](references/security_rules.md). + +## SDK Usage + +To learn how to initialize and use Cloud Firestore in your application code, see [sdk_usage.md](references/sdk_usage.md). diff --git a/skills/firestore-basics/references/provisioning.md b/skills/firestore-basics/references/provisioning.md new file mode 100644 index 00000000000..9b9503cb7db --- /dev/null +++ b/skills/firestore-basics/references/provisioning.md @@ -0,0 +1,38 @@ +# Provisioning Cloud Firestore + +## CLI Initialization + +To set up Firestore in your project directory, use the Firebase CLI: + +```bash +firebase init firestore +``` + +This command will: +1. Ask you to select a default Firebase project (or create a new one). +2. Create a `firestore.rules` file for your security rules. +3. Create a `firestore.indexes.json` file for your index definitions. +4. Update your `firebase.json` configuration file. + +## Configuration (firebase.json) + +Your `firebase.json` should include the `firestore` key pointing to your rules and indexes: + +```json +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + } +} +``` + +## Local Emulation + +To run Firestore locally for development and testing: + +```bash +firebase emulators:start --only firestore +``` + +This starts the Firestore emulator, typically on port 8080. You can interact with it using the Emulator UI (usually at http://localhost:4000/firestore). diff --git a/skills/firestore-basics/references/sdk_usage.md b/skills/firestore-basics/references/sdk_usage.md new file mode 100644 index 00000000000..30a107c3b35 --- /dev/null +++ b/skills/firestore-basics/references/sdk_usage.md @@ -0,0 +1,77 @@ +# Firestore SDK Usage + +## Web (Modular SDK) + +### Initialization + +```javascript +import { initializeApp } from "firebase/app"; +import { getFirestore } from "firebase/firestore"; + +const firebaseConfig = { + // Your config options +}; + +const app = initializeApp(firebaseConfig); +const db = getFirestore(app); +``` + +### connecting to Emulator + +If you are running the local emulator: + +```javascript +import { connectFirestoreEmulator } from "firebase/firestore"; + +// After initializing db +if (location.hostname === "localhost") { + connectFirestoreEmulator(db, 'localhost', 8080); +} +``` + +### Basic Operations + +#### Add Data + +```javascript +import { collection, addDoc } from "firebase/firestore"; + +try { + const docRef = await addDoc(collection(db, "users"), { + first: "Ada", + last: "Lovelace", + born: 1815 + }); + console.log("Document written with ID: ", docRef.id); +} catch (e) { + console.error("Error adding document: ", e); +} +``` + +#### Read Data + +```javascript +import { collection, getDocs } from "firebase/firestore"; + +const querySnapshot = await getDocs(collection(db, "users")); +querySnapshot.forEach((doc) => { + console.log(`${doc.id} => ${doc.data()}`); +}); +``` + +#### Listen for Realtime Updates + +```javascript +import { collection, onSnapshot } from "firebase/firestore"; + +const unsubscribe = onSnapshot(collection(db, "users"), (snapshot) => { + snapshot.docChanges().forEach((change) => { + if (change.type === "added") { + console.log("New user: ", change.doc.data()); + } + }); +}); + +// To stop listening: +// unsubscribe(); +``` diff --git a/skills/firestore-basics/references/security_rules.md b/skills/firestore-basics/references/security_rules.md new file mode 100644 index 00000000000..598b97b6d6d --- /dev/null +++ b/skills/firestore-basics/references/security_rules.md @@ -0,0 +1,58 @@ +# Firestore Security Rules + +Security rules determine who has read and write access to your database. + +## Basic Structure + +Rules are defined in `firestore.rules`. + +```firestore +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + // Rules go here + } +} +``` + +## Common Patterns + +### Locked Mode (Deny All) +Good for starting development or private data. +```firestore +match /{document=**} { + allow read, write: if false; +} +``` + +### Test Mode (Allow All) +**WARNING: insecure.** Only for quick prototyping. +```firestore +match /{document=**} { + allow read, write: if true; +} +``` + +### Auth Required +Allow access only to authenticated users. +```firestore +match /{document=**} { + allow read, write: if request.auth != null; +} +``` + +### User-Specific Data +Allow users to access only their own data. +```firestore +match /users/{userId} { + allow read, write: if request.auth != null && request.auth.uid == userId; +} +``` + +## Deploying Rules + +To deploy only your Firestore rules: + +```bash +firebase deploy --only firestore:rules +``` From 869d13fc9c81b8f30c1a544a290035051dd75b5e Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Thu, 8 Jan 2026 13:13:24 -0800 Subject: [PATCH 2/9] Second pass, with more detail --- .eslintignore | 3 +- skills/firestore-basics/SKILL.md | 13 +- .../references/android_sdk_usage.md | 156 +++++++++++++++ skills/firestore-basics/references/indexes.md | 82 ++++++++ .../references/ios_sdk_usage.md | 188 ++++++++++++++++++ .../references/provisioning.md | 14 ++ .../firestore-basics/references/sdk_usage.md | 77 ------- .../references/security_rules.md | 183 +++++++++++++++-- .../references/web_sdk_usage.md | 179 +++++++++++++++++ 9 files changed, 803 insertions(+), 92 deletions(-) create mode 100644 skills/firestore-basics/references/android_sdk_usage.md create mode 100644 skills/firestore-basics/references/indexes.md create mode 100644 skills/firestore-basics/references/ios_sdk_usage.md delete mode 100644 skills/firestore-basics/references/sdk_usage.md create mode 100644 skills/firestore-basics/references/web_sdk_usage.md diff --git a/.eslintignore b/.eslintignore index 21b7f77de56..6aae848f0b8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,4 +9,5 @@ scripts/agent-evals/output scripts/agent-evals/node_modules scripts/agent-evals/lib scripts/agent-evals/templates -julesbot \ No newline at end of file +julesbot +skills \ No newline at end of file diff --git a/skills/firestore-basics/SKILL.md b/skills/firestore-basics/SKILL.md index 73c166cf35f..81236102099 100644 --- a/skills/firestore-basics/SKILL.md +++ b/skills/firestore-basics/SKILL.md @@ -1,6 +1,7 @@ --- name: firestore-basics description: Comprehensive guide for Firestore basics including provisioning, security rules, and SDK usage. Use this skill when the user needs help setting up Firestore, writing security rules, or using the Firestore SDK in their application. +compatibility: This skill is best used with the Firebase CLI, but does not require it. Install it by running `npm install -g firebase-tools`. --- # Firestore Basics @@ -15,6 +16,14 @@ To set up Cloud Firestore in your Firebase project and local environment, see [p For guidance on writing and deploying Firestore Security Rules to protect your data, see [security_rules.md](references/security_rules.md). -## SDK Usage +## SDK UsageGuides -To learn how to initialize and use Cloud Firestore in your application code, see [sdk_usage.md](references/sdk_usage.md). +To learn how to use Cloud Firestore in your application code, choose your platform: + +* **Web (Modular SDK)**: [web_sdk_usage.md](references/web_sdk_usage.md) +* **Android (Kotlin)**: [android_sdk_usage.md](references/android_sdk_usage.md) +* **iOS (Swift)**: [ios_sdk_usage.md](references/ios_sdk_usage.md) + +## Indexes + +For checking index types, query support tables, and best practices, see [indexes.md](references/indexes.md). diff --git a/skills/firestore-basics/references/android_sdk_usage.md b/skills/firestore-basics/references/android_sdk_usage.md new file mode 100644 index 00000000000..7d113c1dd8b --- /dev/null +++ b/skills/firestore-basics/references/android_sdk_usage.md @@ -0,0 +1,156 @@ +# Firestore Android SDK Usage Guide + +This guide uses **Kotlin** and **KTX extensions**, which correspond to the modern Android development standards. + +## Initialization + +```kotlin +// In your Activity or Application class +import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.ktx.Firebase + +val db = Firebase.firestore + +// Connect to Emulator +// Use 10.0.2.2 to access localhost from the Android Emulator +if (BuildConfig.DEBUG) { + db.useEmulator("10.0.2.2", 8080) +} +``` + +## Writing Data + +### Set a Document (`set`) +Creates or overwrites a document. + +```kotlin +val city = hashMapOf( + "name" to "Los Angeles", + "state" to "CA", + "country" to "USA" +) + +db.collection("cities").document("LA") + .set(city) + .addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully written!") } + .addOnFailureListener { e -> Log.w(TAG, "Error writing document", e) } + +// Merge +db.collection("cities").document("LA") + .set(mapOf("population" to 3900000), SetOptions.merge()) +``` + +### Add a Document with Auto-ID (`add`) + +```kotlin +val data = hashMapOf( + "name" to "Tokyo", + "country" to "Japan" +) + +db.collection("cities") + .add(data) + .addOnSuccessListener { documentReference -> + Log.d(TAG, "DocumentSnapshot written with ID: ${documentReference.id}") + } +``` + +### Update a Document (`update`) + +```kotlin +val laRef = db.collection("cities").document("LA") + +laRef.update("capital", true) + .addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully updated!") } +``` + +### Transactions +Atomic read-modify-write. + +```kotlin +db.runTransaction { transaction -> + val sfDocRef = db.collection("cities").document("SF") + val snapshot = transaction.get(sfDocRef) + + // Note: You can also use FieldValue.increment() for simple counters + val newPopulation = snapshot.getDouble("population")!! + 1 + transaction.update(sfDocRef, "population", newPopulation) + + // Success + null +}.addOnSuccessListener { Log.d(TAG, "Transaction success!") } + .addOnFailureListener { e -> Log.w(TAG, "Transaction failure.", e) } +``` + +## Reading Data + +### Get a Single Document (`get`) + +```kotlin +val docRef = db.collection("cities").document("SF") + +docRef.get().addOnSuccessListener { document -> + if (document != null && document.exists()) { + Log.d(TAG, "DocumentSnapshot data: ${document.data}") + } else { + Log.d(TAG, "No such document") + } +} +``` + +### Get Multiple Documents (`get`) + +```kotlin +db.collection("cities") + .get() + .addOnSuccessListener { result -> + for (document in result) { + Log.d(TAG, "${document.id} => ${document.data}") + } + } +``` + +## Realtime Updates + +### Listen to Changes (`addSnapshotListener`) + +```kotlin +val docRef = db.collection("cities").document("SF") + +docRef.addSnapshotListener { snapshot, e -> + if (e != null) { + Log.w(TAG, "Listen failed.", e) + return@addSnapshotListener + } + + if (snapshot != null && snapshot.exists()) { + val source = if (snapshot.metadata.hasPendingWrites()) "Local" else "Server" + Log.d(TAG, "$source data: ${snapshot.data}") + } else { + Log.d(TAG, "Current data: null") + } +} +``` + +## Queries + +### Simple and Compound +Note: Compound queries on different fields require an index. + +```kotlin +// Simple +db.collection("cities").whereEqualTo("state", "CA") + +// Compound (AND) +db.collection("cities") + .whereEqualTo("state", "CA") + .whereGreaterThan("population", 1000000) +``` + +### Order and Limit + +```kotlin +db.collection("cities") + .orderBy("name", Query.Direction.KEY_ASCENDING) + .limit(3) +``` diff --git a/skills/firestore-basics/references/indexes.md b/skills/firestore-basics/references/indexes.md new file mode 100644 index 00000000000..3cf6fde69a1 --- /dev/null +++ b/skills/firestore-basics/references/indexes.md @@ -0,0 +1,82 @@ +# Firestore Indexes Reference + +Indexes allow Firestore to ensure that query performance depends on the size of the result set, not the size of the database. + +## Index Types + +### Single-Field Indexes +Firestore **automatically creates** a single-field index for every field in a document (and subfields in maps). +* **Support**: Simple equality queries (`==`) and single-field range/sort queries (`<`, `<=`, `orderBy`). +* **Behavior**: You generally don't need to manage these unless you want to *exempt* a field. + +### Composite Indexes +A composite index stores a sorted mapping of all documents based on an ordered list of fields. +* **Support**: Complex queries that filter or sort by **multiple fields**. +* **Creation**: These are **NOT** automatically created. You must define them manually or via the console/CLI. + +## Automatic vs. Manual Management + +### What is Automatic? +* Indexes for simple queries. +* Merging of single-field indexes for multiple equality filters (e.g., `where("state", "==", "CA").where("country", "==", "USA")`). + +### When Do I Need to Act? +If you attempt a query that requires a composite index, the SDK will throw an error containing a **direct link** to the Firebase Console to create that specific index. + +**Example Error:** +> "The query requires an index. You can create it here: https://console.firebase.google.com/project/..." + +## Query Support Examples + +| Query Type | Index Required | +| :--- | :--- | +| **Simple Equality**
`where("a", "==", 1)` | Automatic (Single-Field) | +| **Simple Range/Sort**
`where("a", ">", 1).orderBy("a")` | Automatic (Single-Field) | +| **Multiple Equality**
`where("a", "==", 1).where("b", "==", 2)` | Automatic (Merged Single-Field) | +| **Equality + Range/Sort**
`where("a", "==", 1).where("b", ">", 2)` | **Composite Index** | +| **Multiple Ranges**
`where("a", ">", 1).where("b", ">", 2)` | **Composite Index** (and technically limited query support) | +| **Array Contains + Equality**
`where("tags", "array-contains", "news").where("active", "==", true)` | **Composite Index** | + +## Best Practices & Exemptions + +You can **exempt** fields from automatic indexing to save storage or strictly enforce write limits. + +### 1. High Write Rates (Sequential Values) +* **Problem**: Indexing fields that increase sequentially (like `timestamp`) limits the write rate to ~500 writes/second per collection. +* **Solution**: If you don't query on this field, **exempt** it from simple indexing. + +### 2. Large String/Map/Array Fields +* **Problem**: Indexing limits (40k entries per doc). Indexing large blobs wastes storage. +* **Solution**: Exempt large text blobs or huge arrays if they aren't used for filtering. + +### 3. TTL Fields +* **Problem**: TTL (Time-To-Live) deletion can cause index churn. +* **Solution**: Exempt the TTL timestamp field from indexing if you don't query it. + +## Management + +### `firebase.json` +Your indexes should be defined in `firestore.indexes.json` (pointed to by `firebase.json`). + +```json +{ + "indexes": [ + { + "collectionGroup": "cities", + "queryScope": "COLLECTION", + "fields": [ + { "fieldPath": "country", "order": "ASCENDING" }, + { "fieldPath": "population", "order": "DESCENDING" } + ] + } + ], + "fieldOverrides": [] +} +``` + +### CLI Commands + +Deploy indexes only: +```bash +firebase deploy --only firestore:indexes +``` diff --git a/skills/firestore-basics/references/ios_sdk_usage.md b/skills/firestore-basics/references/ios_sdk_usage.md new file mode 100644 index 00000000000..c6b55e24af8 --- /dev/null +++ b/skills/firestore-basics/references/ios_sdk_usage.md @@ -0,0 +1,188 @@ +# Firestore iOS SDK Usage Guide + +This guide uses **Swift** and the Firebase iOS SDK. + +## Initialization + +```swift +import FirebaseCore +import FirebaseFirestore + +// In your App Delegate or just before using Firestore +FirebaseApp.configure() + +let db = Firestore.firestore() + +// Connect to Emulator (Localhost) +// iOS Simulator uses 'localhost' +#if DEBUG +let settings = db.settings +settings.host = "127.0.0.1:8080" +settings.cacheSettings = MemoryCacheSettings() +settings.isSSLEnabled = false +db.settings = settings +#endif +``` + +## Writing Data + +### Set a Document (`setData`) +Creates or overwrites a document. + +```swift +let city = [ + "name": "Los Angeles", + "state": "CA", + "country": "USA" +] + +db.collection("cities").document("LA").setData(city) { err in + if let err = err { + print("Error writing document: \(err)") + } else { + print("Document successfully written!") + } +} + +// Merge +db.collection("cities").document("LA").setData([ "population": 3900000 ], merge: true) +``` + +### Add a Document with Auto-ID (`addDocument`) + +```swift +var ref: DocumentReference? = nil +ref = db.collection("cities").addDocument(data: [ + "name": "Tokyo", + "country": "Japan" +]) { err in + if let err = err { + print("Error adding document: \(err)") + } else { + print("Document added with ID: \(ref!.documentID)") + } +} +``` + +### Update a Document (`updateData`) + +```swift +let laRef = db.collection("cities").document("LA") + +laRef.updateData([ + "capital": true +]) { err in + if let err = err { + print("Error updating document: \(err)") + } else { + print("Document successfully updated") + } +} +``` + +### Transactions +Atomic read-modify-write. + +```swift +db.runTransaction({ (transaction, errorPointer) -> Any? in + let sfDocument: DocumentSnapshot + do { + try sfDocument = transaction.getDocument(db.collection("cities").document("SF")) + } catch let fetchError as NSError { + errorPointer?.pointee = fetchError + return nil + } + + guard let oldPopulation = sfDocument.data()?["population"] as? Int else { + let error = NSError( + domain: "AppErrorDomain", + code: -1, + userInfo: [ + NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot \(sfDocument)" + ] + ) + errorPointer?.pointee = error + return nil + } + + // Note: You can also use FieldValue.increment(Int64(1)) + transaction.updateData(["population": oldPopulation + 1], forDocument: sfDocument.reference) + return nil +}) { (object, error) in + if let error = error { + print("Transaction failed: \(error)") + } else { + print("Transaction successfully committed!") + } +} +``` + +## Reading Data + +### Get a Single Document (`getDocument`) + +```swift +let docRef = db.collection("cities").document("SF") + +docRef.getDocument { (document, error) in + if let document = document, document.exists { + let dataDescription = document.data().map(String.init(describing:)) ?? "nil" + print("Document data: \(dataDescription)") + } else { + print("Document does not exist") + } +} +``` + +### Get Multiple Documents (`getDocuments`) + +```swift +db.collection("cities").getDocuments() { (querySnapshot, err) in + if let err = err { + print("Error getting documents: \(err)") + } else { + for document in querySnapshot!.documents { + print("\(document.documentID) => \(document.data())") + } + } +} +``` + +## Realtime Updates + +### Listen to Changes (`addSnapshotListener`) + +```swift +db.collection("cities").document("SF") + .addSnapshotListener { documentSnapshot, error in + guard let document = documentSnapshot else { + print("Error fetching document: \(error!)") + return + } + + let source = document.metadata.hasPendingWrites ? "Local" : "Server" + print("\(source) data: \(document.data() ?? [:])") + } +``` + +## Queries + +### Simple and Compound + +```swift +// Simple +db.collection("cities").whereField("state", isEqualTo: "CA") + +// Compound (AND) +db.collection("cities") + .whereField("state", isEqualTo: "CA") + .whereField("population", isGreaterThan: 1000000) +``` + +### Order and Limit + +```swift +db.collection("cities") + .order(by: "name") + .limit(to: 3) +``` diff --git a/skills/firestore-basics/references/provisioning.md b/skills/firestore-basics/references/provisioning.md index 9b9503cb7db..2c1d861a3e6 100644 --- a/skills/firestore-basics/references/provisioning.md +++ b/skills/firestore-basics/references/provisioning.md @@ -27,6 +27,20 @@ Your `firebase.json` should include the `firestore` key pointing to your rules a } ``` +## Deploy rules and indexes +To deploy all rules and indexes +``` +firebase deploy --only firestore +``` +To deploy just rules +``` +firebase deploy --only firestore:rules +``` +To deploy just indexes +``` +firebase deploy --only firestore:indexes +``` + ## Local Emulation To run Firestore locally for development and testing: diff --git a/skills/firestore-basics/references/sdk_usage.md b/skills/firestore-basics/references/sdk_usage.md deleted file mode 100644 index 30a107c3b35..00000000000 --- a/skills/firestore-basics/references/sdk_usage.md +++ /dev/null @@ -1,77 +0,0 @@ -# Firestore SDK Usage - -## Web (Modular SDK) - -### Initialization - -```javascript -import { initializeApp } from "firebase/app"; -import { getFirestore } from "firebase/firestore"; - -const firebaseConfig = { - // Your config options -}; - -const app = initializeApp(firebaseConfig); -const db = getFirestore(app); -``` - -### connecting to Emulator - -If you are running the local emulator: - -```javascript -import { connectFirestoreEmulator } from "firebase/firestore"; - -// After initializing db -if (location.hostname === "localhost") { - connectFirestoreEmulator(db, 'localhost', 8080); -} -``` - -### Basic Operations - -#### Add Data - -```javascript -import { collection, addDoc } from "firebase/firestore"; - -try { - const docRef = await addDoc(collection(db, "users"), { - first: "Ada", - last: "Lovelace", - born: 1815 - }); - console.log("Document written with ID: ", docRef.id); -} catch (e) { - console.error("Error adding document: ", e); -} -``` - -#### Read Data - -```javascript -import { collection, getDocs } from "firebase/firestore"; - -const querySnapshot = await getDocs(collection(db, "users")); -querySnapshot.forEach((doc) => { - console.log(`${doc.id} => ${doc.data()}`); -}); -``` - -#### Listen for Realtime Updates - -```javascript -import { collection, onSnapshot } from "firebase/firestore"; - -const unsubscribe = onSnapshot(collection(db, "users"), (snapshot) => { - snapshot.docChanges().forEach((change) => { - if (change.type === "added") { - console.log("New user: ", change.doc.data()); - } - }); -}); - -// To stop listening: -// unsubscribe(); -``` diff --git a/skills/firestore-basics/references/security_rules.md b/skills/firestore-basics/references/security_rules.md index 598b97b6d6d..63db32fc3b2 100644 --- a/skills/firestore-basics/references/security_rules.md +++ b/skills/firestore-basics/references/security_rules.md @@ -1,41 +1,54 @@ -# Firestore Security Rules +# Firestore Security Rules Structure Security rules determine who has read and write access to your database. -## Basic Structure +## Service and Database Declaration -Rules are defined in `firestore.rules`. +All Firestore rules begin with the service declaration and a match block for the database (usually default). -```firestore +``` rules_version = '2'; + service cloud.firestore { match /databases/{database}/documents { // Rules go here + // {database} wildcard represents the database name } } ``` +## Basic Read/Write Operations + +Rules describe **conditions** that must be true to allow an operation. + +``` +match /cities/{city} { + allow read: if ; + allow write: if ; +} +``` + ## Common Patterns ### Locked Mode (Deny All) Good for starting development or private data. -```firestore +``` match /{document=**} { allow read, write: if false; } ``` ### Test Mode (Allow All) -**WARNING: insecure.** Only for quick prototyping. -```firestore +**WARNING: insecure.** Only for quick prototyping. Unsafe to deploy for production apps. +``` match /{document=**} { allow read, write: if true; } ``` ### Auth Required -Allow access only to authenticated users. -```firestore +Allow access only to authenticated users. This allows any logged in user access to all data. +``` match /{document=**} { allow read, write: if request.auth != null; } @@ -43,15 +56,161 @@ match /{document=**} { ### User-Specific Data Allow users to access only their own data. -```firestore +``` match /users/{userId} { allow read, write: if request.auth != null && request.auth.uid == userId; } ``` -## Deploying Rules +### Allow only verified emails +Requires users to verify ownership of the email address before using it to read or write data +``` + match /databases/{database}/documents { + // Allow access based on email domain + match /some_collection/{document} { + allow read: if request.auth != null + && request.auth.email_verified + && request.auth.email.endsWith('@example.com') + } + } +``` + +### Validate data in write operations +``` +// Example for creating a user profile +match /users/{userId} { + allow create: if request.auth.uid == userId && + request.resource.data.email is string && + request.resource.data.createdAt == request.time; +} +``` + +### Granular Operations + +You can break down `read` and `write` into more specific operations: + +* **read** + * `get`: Retrieval of a single document. + * `list`: Queries and collection reads. +* **write** + * `create`: Writing to a nonexistent document. + * `update`: Writing to an existing document. + * `delete`: Removing a document. + +```firestore +match /cities/{city} { + allow get: if ; + allow list: if ; + allow create: if ; + allow update: if ; + allow delete: if ; +} +``` + +## Hierarchical Data + +Rules applied to a parent collection **do not** cascade to subcollections. You must explicitly match subcollections. + +### Nested Match Statements + +Inner matches are relative to the outer match path. + +```firestore +match /cities/{city} { + allow read, write: if ; + + // Explicitly match the subcollection 'landmarks' + match /landmarks/{landmark} { + allow read, write: if ; + } +} +``` + +### Recursive Wildcards (`{name=**}`) + +Use recursive wildcards to apply rules to an arbitrarily deep hierarchy. + +* **Version 2** (recommended): `{path=**}` matches zero or more path segments. + +```firestore +// Allow read access to ANY document in the 'cities' collection or its subcollections +match /cities/{document=**} { + allow read: if true; +} +``` + +## Controlling Field Access + +### Read Limitations + +Reads in Firestore are **document-level**. You cannot retrieve a partial document. +* **Allowed**: Read the entire document. +* **Denied**: logical failure, no data returned. + +To secure specific fields (e.g., private user data), you must **split them into a separate document** (e.g., a `private` subcollection). + +### Write Restrictions + +You can strictly control which fields can be written or updated. + +#### On Creation +Use `request.resource.data.keys()` to validate fields. + +```firestore +match /restaurant/{restId} { + allow create: if request.resource.data.keys().hasAll(['name', 'location']) && + request.resource.data.keys().hasOnly(['name', 'location', 'city', 'address']); +} +``` + +#### On Update +Use `diff()` to see what changed between the existing document (`resource.data`) and the incoming data (`request.resource.data`). + +```firestore +match /restaurant/{restId} { + allow update: if request.resource.data.diff(resource.data).affectedKeys() + .hasOnly(['name', 'location', 'city']); // Prevent others from changing +} +``` + +### Enforcing Field Types +Use the `is` operator to validate data types. + +```firestore +allow create: if request.resource.data.score is int && + request.resource.data.active is bool && + request.resource.data.tags is list; +``` + +## Understanding Rule Evaluation + +### Overlapping Matches -> OR Logic + +If a document matches more than one rule statement, access is allowed if **ANY** of the matching rules allow it. + +```firestore +// Document: /cities/SF + +match /cities/{city} { + allow read: if false; // Deny +} + +match /cities/{document=**} { + allow read: if true; // Allow +} + +// Result: ALLOWED (because one rule returned true) +``` + +## Common Limits + +* **Call Depth**: Maximum call depth for custom functions is 20. +* **Document Access**: + * 10 access calls for single-doc requests/queries. + * 20 access calls for multi-doc reads/transactions/batches. +* **Size**: Ruleset source max 256 KB. Compiled max 250 KB. -To deploy only your Firestore rules: +## Deploying ```bash firebase deploy --only firestore:rules diff --git a/skills/firestore-basics/references/web_sdk_usage.md b/skills/firestore-basics/references/web_sdk_usage.md new file mode 100644 index 00000000000..29f6ee6db24 --- /dev/null +++ b/skills/firestore-basics/references/web_sdk_usage.md @@ -0,0 +1,179 @@ +# Firestore Web SDK Usage Guide + +This guide focuses on the **Modular Web SDK** (v9+), which is tree-shakeable and efficient. + +## Initialization + +import { initializeApp } from "firebase/app"; +import { getFirestore } from "firebase/firestore"; + +const firebaseConfig = { + // Your config options. Get the values by running 'firebase apps:sdkconfig ' +}; + +const app = initializeApp(firebaseConfig); +const db = getFirestore(app); + +``` + +## Writing Data + +### Set a Document (`setDoc`) +Creates a document if it doesn't exist, or overwrites it if it does. + +```javascript +import { doc, setDoc } from "firebase/firestore"; + +// Create/Overwrite document with ID "LA" +await setDoc(doc(db, "cities", "LA"), { + name: "Los Angeles", + state: "CA", + country: "USA" +}); + +// To merge with existing data instead of overwriting: +await setDoc(doc(db, "cities", "LA"), { population: 3900000 }, { merge: true }); +``` + +### Add a Document with Auto-ID (`addDoc`) +Use when you don't care about the document ID. + +```javascript +import { collection, addDoc } from "firebase/firestore"; + +const docRef = await addDoc(collection(db, "cities"), { + name: "Tokyo", + country: "Japan" +}); +console.log("Document written with ID: ", docRef.id); +``` + +### Update a Document (`updateDoc`) +Update some fields of an existing document without overwriting the entire document. Fails if the document doesn't exist. + +```javascript +import { doc, updateDoc } from "firebase/firestore"; + +const laRef = doc(db, "cities", "LA"); + +await updateDoc(laRef, { + capital: true +}); +``` + +### Transactions +Perform an atomic read-modify-write operation. + +```javascript +import { runTransaction, doc } from "firebase/firestore"; + +const sfDocRef = doc(db, "cities", "SF"); + +try { + await runTransaction(db, async (transaction) => { + const sfDoc = await transaction.get(sfDocRef); + if (!sfDoc.exists()) { + throw "Document does not exist!"; + } + + const newPopulation = sfDoc.data().population + 1; + transaction.update(sfDocRef, { population: newPopulation }); + }); + console.log("Transaction successfully committed!"); +} catch (e) { + console.log("Transaction failed: ", e); +} +``` + +## Reading Data + +### Get a Single Document (`getDoc`) + +```javascript +import { doc, getDoc } from "firebase/firestore"; + +const docRef = doc(db, "cities", "SF"); +const docSnap = await getDoc(docRef); + +if (docSnap.exists()) { + console.log("Document data:", docSnap.data()); +} else { + console.log("No such document!"); +} +``` + +### Get Multiple Documents (`getDocs`) +Fetches all documents in a query or collection once. + +```javascript +import { collection, getDocs } from "firebase/firestore"; + +const querySnapshot = await getDocs(collection(db, "cities")); +querySnapshot.forEach((doc) => { + // doc.data() is never undefined for query doc snapshots + console.log(doc.id, " => ", doc.data()); +}); +``` + +## Realtime Updates + +### Listen to a Document/Query (`onSnapshot`) + +```javascript +import { doc, onSnapshot } from "firebase/firestore"; + +const unsub = onSnapshot(doc(db, "cities", "SF"), (doc) => { + console.log("Current data: ", doc.data()); +}); + +// Stop listening +// unsub(); +``` + +### Handle Changes (Added/Modified/Removed) + +```javascript +import { collection, query, where, onSnapshot } from "firebase/firestore"; + +const q = query(collection(db, "cities"), where("state", "==", "CA")); +const unsubscribe = onSnapshot(q, (snapshot) => { + snapshot.docChanges().forEach((change) => { + if (change.type === "added") { + console.log("New city: ", change.doc.data()); + } + if (change.type === "modified") { + console.log("Modified city: ", change.doc.data()); + } + if (change.type === "removed") { + console.log("Removed city: ", change.doc.data()); + } + }); +}); +``` + +## Queries + +### Simple and Compound Queries +Use `query()` to combine filters. + +```javascript +import { collection, query, where, getDocs } from "firebase/firestore"; + +const citiesRef = collection(db, "cities"); + +// Simple equality +const q1 = query(citiesRef, where("state", "==", "CA")); + +// Compound (AND) +// Note: Requires an index if filtering on different fields +const q2 = query(citiesRef, where("state", "==", "CA"), where("population", ">", 1000000)); +``` + +### Order and Limit +Sort and limit results. + +```javascript +import { orderBy, limit } from "firebase/firestore"; + +const q = query(citiesRef, orderBy("name"), limit(3)); +``` From 1535b51fe25cce8f6090530b7e1d5a9f9c753ae7 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Fri, 9 Jan 2026 09:03:53 -0800 Subject: [PATCH 3/9] More skill improvements --- skills/firestore-basics/SKILL.md | 2 +- .../references/provisioning.md | 77 +++++++++++++++---- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/skills/firestore-basics/SKILL.md b/skills/firestore-basics/SKILL.md index 81236102099..24c4d0aaad5 100644 --- a/skills/firestore-basics/SKILL.md +++ b/skills/firestore-basics/SKILL.md @@ -16,7 +16,7 @@ To set up Cloud Firestore in your Firebase project and local environment, see [p For guidance on writing and deploying Firestore Security Rules to protect your data, see [security_rules.md](references/security_rules.md). -## SDK UsageGuides +## SDK Usage To learn how to use Cloud Firestore in your application code, choose your platform: diff --git a/skills/firestore-basics/references/provisioning.md b/skills/firestore-basics/references/provisioning.md index 2c1d861a3e6..3484f895c54 100644 --- a/skills/firestore-basics/references/provisioning.md +++ b/skills/firestore-basics/references/provisioning.md @@ -1,22 +1,16 @@ # Provisioning Cloud Firestore -## CLI Initialization +## Manual Initialization -To set up Firestore in your project directory, use the Firebase CLI: +For non-interactive environments (like AI agents), it is recommended to manually create the necessary configuration files instead of using `firebase init`. -```bash -firebase init firestore -``` - -This command will: -1. Ask you to select a default Firebase project (or create a new one). -2. Create a `firestore.rules` file for your security rules. -3. Create a `firestore.indexes.json` file for your index definitions. -4. Update your `firebase.json` configuration file. +1. **Create `firebase.json`**: This file configures the Firebase CLI. +2. **Create `firestore.rules`**: This file contains your security rules. +3. **Create `firestore.indexes.json`**: This file contains your index definitions. -## Configuration (firebase.json) +### 1. Create `firebase.json` -Your `firebase.json` should include the `firestore` key pointing to your rules and indexes: +Create a file named `firebase.json` in your project root with the following content. If this file already exists, instead append to the existing JSON: ```json { @@ -27,6 +21,63 @@ Your `firebase.json` should include the `firestore` key pointing to your rules a } ``` +This will use the default database. To use a different database, specify the database ID and location. You can check the list of available databases using `firebase firestore:databases:list`. If the database does not exist, it will be created when you deploy: + +```json +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json", + "database": "my-database-id", + "location": "us-central1" + } +} +``` + + To use Enterprise edition, specify the `enterprise` field. + +```json +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json", + "enterprise": true, + "database": "my-database-id", + "location": "us-central1" + } +} +``` + +### 2. Create `firestore.rules` + +Create a file named `firestore.rules`. A good starting point (locking down the database) is: + +``` +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if false; + } + } +} +``` +*See [security_rules.md](security_rules.md) for how to write actual rules.* + +### 3. Create `firestore.indexes.json` + +Create a file named `firestore.indexes.json` with an empty configuration to start: + +```json +{ + "indexes": [], + "fieldOverrides": [] +} +``` + +*See [indexes.md](indexes.md) for how to configure indexes.* + + ## Deploy rules and indexes To deploy all rules and indexes ``` From cdf47bf315271b98d8fa82a7aeb59844fe6b927c Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Fri, 9 Jan 2026 09:05:01 -0800 Subject: [PATCH 4/9] Adding a script to count tokens used by a skill --- scripts/skill-token-counter/index.ts | 83 + scripts/skill-token-counter/package-lock.json | 1506 +++++++++++++++++ scripts/skill-token-counter/package.json | 19 + scripts/skill-token-counter/tsconfig.json | 13 + 4 files changed, 1621 insertions(+) create mode 100644 scripts/skill-token-counter/index.ts create mode 100644 scripts/skill-token-counter/package-lock.json create mode 100644 scripts/skill-token-counter/package.json create mode 100644 scripts/skill-token-counter/tsconfig.json diff --git a/scripts/skill-token-counter/index.ts b/scripts/skill-token-counter/index.ts new file mode 100644 index 00000000000..8abba614154 --- /dev/null +++ b/scripts/skill-token-counter/index.ts @@ -0,0 +1,83 @@ +import * as genai from "@google/genai"; +import matter from "gray-matter"; +import { glob } from "glob"; +import * as fs from "fs"; +import * as path from "path"; + +const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY; + +if (!apiKey) { + console.error("Error: GEMINI_API_KEY or GOOGLE_API_KEY environment variable is required."); + process.exit(1); +} + +const client = new genai.GoogleGenAI({ apiKey }); + +const modelId = "gemini-3-pro-preview"; + +async function countTokens(text: string): Promise { + if (!text) return 0; + try { + const response = await client.models.countTokens({ + model: modelId, + contents: [{ parts: [{ text }] }], + }); + return response.totalTokens || 0; + } catch (error) { + console.warn("Failed to count tokens:", error); + return 0; + } +} + +async function main() { + const skillDir = process.argv[2]; + if (!skillDir) { + console.error("Usage: npm run count "); + process.exit(1); + } + + const skillMdPath = path.join(skillDir, "SKILL.md"); + if (!fs.existsSync(skillMdPath)) { + console.error(`Error: SKILL.md not found in ${skillDir}`); + process.exit(1); + } + + console.log(`Analyzing skill at: ${skillDir}\n`); + // Analyze SKILL.md + const skillContent = fs.readFileSync(skillMdPath, "utf-8"); + const parsed = matter(skillContent); + const frontmatterString = JSON.stringify(parsed.data, null, 2); + const bodyString = parsed.content; + + const frontmatterTokens = await countTokens(frontmatterString); + const bodyTokens = await countTokens(bodyString); + + console.log(`SKILL.md Frontmatter: ${frontmatterTokens} tokens`); + console.log(`SKILL.md Body: ${bodyTokens} tokens`); + + let totalTokens = frontmatterTokens + bodyTokens; + + // Analyze references + const referencesPattern = path.join(skillDir, "references", "*.md"); + const referenceFiles = await glob(referencesPattern); + + if (referenceFiles.length > 0) { + console.log("\nReferences:"); + for (const refFile of referenceFiles) { + const content = fs.readFileSync(refFile, "utf-8"); + const tokens = await countTokens(content); + totalTokens += tokens; + const filename = path.basename(refFile); + console.log(` ${filename.padEnd(25)} ${tokens} tokens`); + } + } else { + console.log("\nNo references found in references/"); + } + + console.log(`\nTotal Tokens: ${totalTokens}`); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/skill-token-counter/package-lock.json b/scripts/skill-token-counter/package-lock.json new file mode 100644 index 00000000000..13f65ddb9d3 --- /dev/null +++ b/scripts/skill-token-counter/package-lock.json @@ -0,0 +1,1506 @@ +{ + "name": "skill-token-counter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "skill-token-counter", + "version": "1.0.0", + "dependencies": { + "@google/genai": "^1.35.0", + "glob": "^10.3.10", + "gray-matter": "^4.0.3", + "tsx": "^4.11.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.3.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.35.0.tgz", + "integrity": "sha512-ZC1d0PSM5eS73BpbVIgL3ZsmXeMKLVJurxzww1Z9axy3B2eUB3ioEytbQt4Qu0Od6qPluKrTDew9pSi9kEuPaw==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.24.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/scripts/skill-token-counter/package.json b/scripts/skill-token-counter/package.json new file mode 100644 index 00000000000..24bd1bf4af2 --- /dev/null +++ b/scripts/skill-token-counter/package.json @@ -0,0 +1,19 @@ +{ + "name": "skill-token-counter", + "version": "1.0.0", + "description": "Utility to count tokens in a skill definition", + "main": "index.ts", + "scripts": { + "count": "tsx index.ts" + }, + "dependencies": { + "@google/genai": "^1.35.0", + "glob": "^10.3.10", + "gray-matter": "^4.0.3", + "tsx": "^4.11.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.3.3" + } +} \ No newline at end of file diff --git a/scripts/skill-token-counter/tsconfig.json b/scripts/skill-token-counter/tsconfig.json new file mode 100644 index 00000000000..7119cd5a4ee --- /dev/null +++ b/scripts/skill-token-counter/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist" + }, + "include": ["**/*.ts"], + "exclude": ["node_modules"] +} From cdef2a6ec501c995615300f6c86e2f2a08f3cdbb Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Fri, 9 Jan 2026 09:29:16 -0800 Subject: [PATCH 5/9] Parallelize --- scripts/skill-token-counter/index.ts | 29 +++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/scripts/skill-token-counter/index.ts b/scripts/skill-token-counter/index.ts index 8abba614154..027c822e5ac 100644 --- a/scripts/skill-token-counter/index.ts +++ b/scripts/skill-token-counter/index.ts @@ -49,8 +49,10 @@ async function main() { const frontmatterString = JSON.stringify(parsed.data, null, 2); const bodyString = parsed.content; - const frontmatterTokens = await countTokens(frontmatterString); - const bodyTokens = await countTokens(bodyString); + const [frontmatterTokens, bodyTokens] = await Promise.all([ + countTokens(frontmatterString), + countTokens(bodyString), + ]); console.log(`SKILL.md Frontmatter: ${frontmatterTokens} tokens`); console.log(`SKILL.md Body: ${bodyTokens} tokens`); @@ -63,12 +65,21 @@ async function main() { if (referenceFiles.length > 0) { console.log("\nReferences:"); - for (const refFile of referenceFiles) { - const content = fs.readFileSync(refFile, "utf-8"); - const tokens = await countTokens(content); - totalTokens += tokens; - const filename = path.basename(refFile); - console.log(` ${filename.padEnd(25)} ${tokens} tokens`); + + const results = await Promise.all( + referenceFiles.map(async (refFile) => { + const content = fs.readFileSync(refFile, "utf-8"); + const tokens = await countTokens(content); + return { + filename: path.basename(refFile), + tokens, + }; + }), + ); + + for (const result of results) { + totalTokens += result.tokens; + console.log(` ${result.filename.padEnd(25)} ${result.tokens} tokens`); } } else { console.log("\nNo references found in references/"); @@ -77,7 +88,7 @@ async function main() { console.log(`\nTotal Tokens: ${totalTokens}`); } -main().catch(err => { +main().catch((err) => { console.error(err); process.exit(1); }); From c5937b46c28536cdf9b8564e65a1444f3a1a0a41 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Fri, 9 Jan 2026 09:37:58 -0800 Subject: [PATCH 6/9] Omit skills from prettier --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index 4b61cb58ecf..2f612bc6250 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,6 +8,7 @@ /scripts/agent-evals/output/** /src/frameworks/docs/** /prompts +/skills # Intentionally invalid YAML file: /src/test/fixtures/extension-yamls/invalid/extension.yaml From 766970ad63abada5114cf257b76b06175a0a16d9 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Fri, 9 Jan 2026 11:26:31 -0800 Subject: [PATCH 7/9] PR fixes --- .../references/android_sdk_usage.md | 5 +++-- .../firestore-basics/references/ios_sdk_usage.md | 6 +++--- skills/firestore-basics/references/provisioning.md | 14 ++++++-------- .../firestore-basics/references/security_rules.md | 14 ++++++-------- .../firestore-basics/references/web_sdk_usage.md | 2 ++ 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/skills/firestore-basics/references/android_sdk_usage.md b/skills/firestore-basics/references/android_sdk_usage.md index 7d113c1dd8b..82b91de5179 100644 --- a/skills/firestore-basics/references/android_sdk_usage.md +++ b/skills/firestore-basics/references/android_sdk_usage.md @@ -7,6 +7,7 @@ This guide uses **Kotlin** and **KTX extensions**, which correspond to the moder ```kotlin // In your Activity or Application class import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.firestore.SetOptions import com.google.firebase.ktx.Firebase val db = Firebase.firestore @@ -73,7 +74,7 @@ db.runTransaction { transaction -> val snapshot = transaction.get(sfDocRef) // Note: You can also use FieldValue.increment() for simple counters - val newPopulation = snapshot.getDouble("population")!! + 1 + val newPopulation = (snapshot.getDouble("population") ?: 0.0) + 1 transaction.update(sfDocRef, "population", newPopulation) // Success @@ -151,6 +152,6 @@ db.collection("cities") ```kotlin db.collection("cities") - .orderBy("name", Query.Direction.KEY_ASCENDING) + .orderBy("name", Query.Direction.ASCENDING) .limit(3) ``` diff --git a/skills/firestore-basics/references/ios_sdk_usage.md b/skills/firestore-basics/references/ios_sdk_usage.md index c6b55e24af8..e76c1532f66 100644 --- a/skills/firestore-basics/references/ios_sdk_usage.md +++ b/skills/firestore-basics/references/ios_sdk_usage.md @@ -48,7 +48,7 @@ db.collection("cities").document("LA").setData(city) { err in db.collection("cities").document("LA").setData([ "population": 3900000 ], merge: true) ``` -### Add a Document with Auto-ID (`addDocument`) +###// Add a Document with Auto-ID (`addDocument`) ```swift var ref: DocumentReference? = nil @@ -59,7 +59,7 @@ ref = db.collection("cities").addDocument(data: [ if let err = err { print("Error adding document: \(err)") } else { - print("Document added with ID: \(ref!.documentID)") + print("Document added with ID: \(ref?.documentID ?? "unknown")") } } ``` @@ -141,7 +141,7 @@ db.collection("cities").getDocuments() { (querySnapshot, err) in if let err = err { print("Error getting documents: \(err)") } else { - for document in querySnapshot!.documents { + for document in querySnapshot?.documents ?? [] { print("\(document.documentID) => \(document.data())") } } diff --git a/skills/firestore-basics/references/provisioning.md b/skills/firestore-basics/references/provisioning.md index 3484f895c54..3a6882f9a61 100644 --- a/skills/firestore-basics/references/provisioning.md +++ b/skills/firestore-basics/references/provisioning.md @@ -79,16 +79,14 @@ Create a file named `firestore.indexes.json` with an empty configuration to star ## Deploy rules and indexes -To deploy all rules and indexes -``` +```bash +# To deploy all rules and indexes firebase deploy --only firestore -``` -To deploy just rules -``` + +# To deploy just rules firebase deploy --only firestore:rules -``` -To deploy just indexes -``` + +# To deploy just indexes firebase deploy --only firestore:indexes ``` diff --git a/skills/firestore-basics/references/security_rules.md b/skills/firestore-basics/references/security_rules.md index 63db32fc3b2..5ed40ad703a 100644 --- a/skills/firestore-basics/references/security_rules.md +++ b/skills/firestore-basics/references/security_rules.md @@ -65,14 +65,12 @@ match /users/{userId} { ### Allow only verified emails Requires users to verify ownership of the email address before using it to read or write data ``` - match /databases/{database}/documents { - // Allow access based on email domain - match /some_collection/{document} { - allow read: if request.auth != null - && request.auth.email_verified - && request.auth.email.endsWith('@example.com') - } - } +// Allow access based on email domain +match /some_collection/{document} { + allow read: if request.auth != null + && request.auth.email_verified + && request.auth.email.endsWith('@example.com'); +} ``` ### Validate data in write operations diff --git a/skills/firestore-basics/references/web_sdk_usage.md b/skills/firestore-basics/references/web_sdk_usage.md index 29f6ee6db24..64510ab8998 100644 --- a/skills/firestore-basics/references/web_sdk_usage.md +++ b/skills/firestore-basics/references/web_sdk_usage.md @@ -4,8 +4,10 @@ This guide focuses on the **Modular Web SDK** (v9+), which is tree-shakeable and ## Initialization +```javascript import { initializeApp } from "firebase/app"; import { getFirestore } from "firebase/firestore"; +``` const firebaseConfig = { // Your config options. Get the values by running 'firebase apps:sdkconfig ' From 75f3a9a852d4adf5a720165fdf24038117344dd3 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Mon, 12 Jan 2026 14:51:23 -0800 Subject: [PATCH 8/9] Starting on data connect skill --- skills/data-connect/SKILL.md | 109 ++++++++++++++++++ .../references/mutations_example.gql | 33 ++++++ .../references/queries_example.gql | 79 +++++++++++++ .../references/schema_example.gql | 52 +++++++++ 4 files changed, 273 insertions(+) create mode 100644 skills/data-connect/SKILL.md create mode 100644 skills/data-connect/references/mutations_example.gql create mode 100644 skills/data-connect/references/queries_example.gql create mode 100644 skills/data-connect/references/schema_example.gql diff --git a/skills/data-connect/SKILL.md b/skills/data-connect/SKILL.md new file mode 100644 index 00000000000..9dc203b9f1c --- /dev/null +++ b/skills/data-connect/SKILL.md @@ -0,0 +1,109 @@ +--- +name: firebase-data-connect +description: Comprehensive guide for developing with Firebase Data Connect. Use this skill when users need to: (1) Provision a new Data Connect service, (2) Write Data Connect schemas (.gql files with @table), (3) Write queries and mutations, or (4) Generate and use typed SDKs. +--- + +# Firebase Data Connect + +Firebase Data Connect maps GraphQL to Cloud SQL (PostgreSQL), providing typed interactions and local development tools. + +## Project Structure & Configuration + +``` +dataconnect/ +├── dataconnect.yaml # Main service configuration. Required. +├── schema/ +│ └── schema.gql # GraphQL schema with @table definitions. Required. +└── connector/ + ├── connector.yaml # Connector configuration. Required. + ├── queries.gql # Any .GQL files in this directory will be included in the connector. + └── mutations.gql +``` + +### Service Configuration (`dataconnect.yaml`) + +Defines the service, location, and database connection. Replace the values with your own. + +```yaml +specVersion: "v1" +serviceId: "my-service" +location: "us-east4" +schema: + source: "./schema" + datasource: + postgresql: + database: "fdcdb" + cloudSql: + instanceId: "my-project-id:us-east4:my-instance" +connectorDirs: ["./connector"] +``` + +### Connector Configuration (`connector.yaml`) + +Defines the connector ID and SDK generation settings. + +```yaml +connectorId: "my-connector" +generate: + javascriptSdk: + outputDir: "../../js/generated" + package: "@firebasegen/default-connector" +``` + +## Schema Definition (`schema.gql`) + +Data Connect schemas use GraphQL syntax with the `@table` directive to map types to PostgreSQL tables. + +### Key Concepts + +* **@table**: Helper directive to map a type to a table. +* **@col**: Helper directive to customize column definition (e.g., `dataType`, `name`). +* **@default**: Helper directive to set default values (e.g., `expr: "auth.uid"`, `expr: "request.time"`). +* **Relationships**: + * **One-to-Many**: Define a field of the related type in the "Many" side table. + * **One-to-One**: Use `@unique` on the foreign key field. + * **Many-to-Many**: Create a join table with composite keys. + + +## Writing Schemas and Operations + +Follow this iterative workflow to ensure correctness: + +1. **Write Schema**: Define your types in `schema/schema.gql`. +2. **Validate Schema**: Run `firebase dataconnect:compile`. + * Fix any errors reported. + * Repeat until compilation succeeds. +3. **Inspect Generated Types**: Read the contents of `.dataconnect/` to understand the generated type definitions. +4. **Write Operations**: Create queries and mutations in `connector/` (e.g., `queries.gql`). +5. **Validate Operations**: Run `firebase dataconnect:compile`. + * Fix any errors. + * Repeat until compilation succeeds. +6. **Test**: Write unit tests to validate that each operation behaves as expected. + +### Example GQL + +See [schema_example.gql](references/schema_example.gql). + +See [queries_example.gql](references/queries_example.gql) for examples of listing, filtering, and joining data. + +See [mutations_example.gql](references/mutations_example.gql) for examples of creating, updating (upsert), and deleting data securely. + +### Key Directives + +* **@auth(level: ...)**: Controls access level. + * `PUBLIC`: Accessible by anyone (requires `insecureReason`). + * `USER`: Accessible by any authenticated user. + * `USER_EMAIL_VERIFIED`: Accessible by potential verified users. + * `NO_ACCESS`: Admin only (internal use). + * **Note**: You can also use `id_expr: "auth.uid"` in filters/data to restrict access to the specific user. + +## SDK Generation + +Data Connect generates typed SDKs for your client apps (Web, Android, iOS, Dart). + +1. **Configure**: Ensure `connector.yaml` has the `generate` block (as shown above). +2. **Generate**: Run `firebase dataconnect:sdk:generate`. + * Use `--watch` to auto-regenerate on changes. +3. **Use in App**: + * Import the generated connector and operations. + * Call operation functions (e.g., `listMovies()`, `createMovie(...)`). diff --git a/skills/data-connect/references/mutations_example.gql b/skills/data-connect/references/mutations_example.gql new file mode 100644 index 00000000000..1ae8a1926a2 --- /dev/null +++ b/skills/data-connect/references/mutations_example.gql @@ -0,0 +1,33 @@ +# Example mutations for a simple movie app + +# Create a movie based on user input +mutation CreateMovie($title: String!, $genre: String!, $imageUrl: String!) +@auth(level: USER_EMAIL_VERIFIED, insecureReason: "Any email verified users can create a new movie.") { + movie_insert(data: { title: $title, genre: $genre, imageUrl: $imageUrl }) +} + +# Upsert (update or insert) a user's username based on their auth.uid +mutation UpsertUser($username: String!) @auth(level: USER) { + # The "auth.uid" server value ensures that users can only register their own user. + user_upsert(data: { id_expr: "auth.uid", username: $username }) +} + +# Add a review for a movie +mutation AddReview($movieId: UUID!, $rating: Int!, $reviewText: String!) +@auth(level: USER) { + review_upsert( + data: { + userId_expr: "auth.uid" + movieId: $movieId + rating: $rating + reviewText: $reviewText + # reviewDate defaults to today in the schema. No need to set it manually. + } + ) +} + +# Logged in user can delete their review for a movie +mutation DeleteReview($movieId: UUID!) @auth(level: USER) { + # The "auth.uid" server value ensures that users can only delete their own reviews. + review_delete(key: { userId_expr: "auth.uid", movieId: $movieId }) +} diff --git a/skills/data-connect/references/queries_example.gql b/skills/data-connect/references/queries_example.gql new file mode 100644 index 00000000000..5cb4add5e90 --- /dev/null +++ b/skills/data-connect/references/queries_example.gql @@ -0,0 +1,79 @@ +# Example queries for a simple movie app. + +# @auth() directives control who can call each operation. +# Anyone should be able to list all movies, so the auth level is set to PUBLIC +query ListMovies @auth(level: PUBLIC, insecureReason: "Anyone can list all movies.") { + movies { + id + title + imageUrl + genre + } +} + +# List all users, only admins should be able to list all users, so we use NO_ACCESS +query ListUsers @auth(level: NO_ACCESS) { + users { + id + username + } +} + +# Logged in users can list all their reviews and movie titles associated with the review +# Since the query uses the uid of the current authenticated user, we set auth level to USER +query ListUserReviews @auth(level: USER) { + user(key: { id_expr: "auth.uid" }) { + id + username + # _on_ makes it easy to grab info from another table + # Here, we use it to grab all the reviews written by the user. + reviews: reviews_on_user { + rating + reviewDate + reviewText + movie { + id + title + } + } + } +} + +# Get movie by id +query GetMovieById($id: UUID!) @auth(level: PUBLIC, insecureReason: "Anyone can get a movie by id.") { + movie(id: $id) { + id + title + imageUrl + genre + metadata: movieMetadata_on_movie { + rating + releaseYear + description + # metadata is valid only if movie exists + } + reviews: reviews_on_movie { + reviewText + reviewDate + rating + user { + id + username + } + } + } +} + +# Search for movies, actors, and reviews +query SearchMovie($titleInput: String, $genre: String) @auth(level: PUBLIC, insecureReason: "Anyone can search for movies.") { + movies( + where: { + _and: [{ genre: { eq: $genre } }, { title: { contains: $titleInput } }] + } + ) { + id + title + genre + imageUrl + } +} diff --git a/skills/data-connect/references/schema_example.gql b/skills/data-connect/references/schema_example.gql new file mode 100644 index 00000000000..9ceb0373545 --- /dev/null +++ b/skills/data-connect/references/schema_example.gql @@ -0,0 +1,52 @@ +# Example schema for simple movie review app + +# User table is keyed by Firebase Auth UID. +type User @table { + # `@default(expr: "auth.uid")` sets it to Firebase Auth UID during insert and upsert. + id: String! @default(expr: "auth.uid") + username: String! @col(dataType: "varchar(50)") + # The `user: User!` field in the Review table generates the following one-to-many query field. + # reviews_on_user: [Review!]! + # The `Review` join table the following many-to-many query field. + # movies_via_Review: [Movie!]! +} + +# Movie is keyed by a randomly generated UUID. +type Movie @table { + # If you do not pass a 'key' to `@table`, Data Connect automatically adds the following 'id' column. + # Feel free to uncomment and customize it. + # id: UUID! @default(expr: "uuidV4()") + title: String! + imageUrl: String! + genre: String +} + +# MovieMetadata is a metadata attached to a Movie. +# Movie <-> MovieMetadata is a one-to-one relationship +type MovieMetadata @table { + # @unique ensures each Movie can only one MovieMetadata. + movie: Movie! @unique + # The movie field adds the following foreign key field. Feel free to uncomment and customize it. + # movieId: UUID! + rating: Float + releaseYear: Int + description: String +} + +# Reviews is a join table between User and Movie. +# It has a composite primary keys `userUid` and `movieId`. +# A user can leave reviews for many movies. A movie can have reviews from many users. +# User <-> Review is a one-to-many relationship +# Movie <-> Review is a one-to-many relationship +# Movie <-> User is a many-to-many relationship +type Review @table(name: "Reviews", key: ["movie", "user"]) { + user: User! + # The user field adds the following foreign key field. Feel free to uncomment and customize it. + # userUid: String! + movie: Movie! + # The movie field adds the following foreign key field. Feel free to uncomment and customize it. + # movieId: UUID! + rating: Int + reviewText: String + reviewDate: Date! @default(expr: "request.time") +} From 078b2617686217c20f17ff174a15b5cf4a22a2f7 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Mon, 12 Jan 2026 18:47:56 -0800 Subject: [PATCH 9/9] Fix frontmatter error --- skills/data-connect/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/data-connect/SKILL.md b/skills/data-connect/SKILL.md index 9dc203b9f1c..a5a9b02de91 100644 --- a/skills/data-connect/SKILL.md +++ b/skills/data-connect/SKILL.md @@ -1,6 +1,6 @@ --- -name: firebase-data-connect -description: Comprehensive guide for developing with Firebase Data Connect. Use this skill when users need to: (1) Provision a new Data Connect service, (2) Write Data Connect schemas (.gql files with @table), (3) Write queries and mutations, or (4) Generate and use typed SDKs. +name: data-connect-basics +description: Comprehensive guide for developing with Firebase Data Connect. Use this skill when users need to (1) Provision a new Data Connect service, (2) Write Data Connect schemas (.gql files with @table), (3) Write queries and mutations, or (4) Generate and use typed SDKs. --- # Firebase Data Connect