diff --git a/skills/firebase-v1-v2-migration/SKILL.md b/skills/firebase-v1-v2-migration/SKILL.md new file mode 100644 index 00000000..2497415b --- /dev/null +++ b/skills/firebase-v1-v2-migration/SKILL.md @@ -0,0 +1,138 @@ +--- +name: firebase-v1-v2-migration +description: Use this skill when a user wants to upgrade their legacy Firebase Functions from V1 (GCF 1st Gen) to V2 (GCF 2nd Gen) safely without rewriting their internal business logic. This skill relies on the Destructuring Compatibility Shim. +--- + +# 🚀 Supported Migration Strategies + +This skill currently supports the **In-Place Migration** strategy. + +### 🚜 In-Place Migration (Standard) + +The agent modifies the existing V1 code file directly to use V2 syntax and +overwrites the deployment slot. + +- **Pros**: Fast, simple, clean repository history. +- **Cons**: No safety net during deployment. If the V2 deployment fails, you + must rollback using Git. + +*Note: For complex or zero-downtime migrations (e.g. Side-by-Side deployment), +refer to external orchestration skills.* + +# Prerequisites + +Please ensure the workspace is ready for V2 before attempting a code migration: + +1. **Configuration Check**: Ensure the workspace has transitioned away from + functions.config() to Parameterized Configuration or standard environment + variables. +1. **Dependencies**: The project must be using firebase-functions version that + supports V2 (>= 4.0.0). + +# 🔍 Pre-Migration Checklist + +Before modifying any code, the agent should run a quick scan: + +1. **Scan for legacy configs**: Run a `grep` or text search for usages of + `functions.config()`. + - **Action**: If found, **stop and warn the user** that these configs will + evaluate to `undefined` in V2 unless they migrate to Parameterized + Configuration or standard `.env` variables first. + +# Principles of Safe Migration + +Always follow these principles to ensure zero-touch logic migration: + +1. **Use Context-Aware Editing over Global Regex**: Never use naive + find-and-replace. Rely on syntax-aware editing (such as an AI agent reading + the file context and making precise edits, or tools like ts-morph/ast + parsers) to ensure context isolation. +1. **Signature Modernization with Destructuring**: Do NOT rewrite the internal + variable usages of context or params inside the function body. Instead, use + JavaScript's native object destructuring in the new V2 signature parameters. + (Note: For `https.onCall`, the context shim is not available; you should + destructure `auth` and `data` directly from the request object instead of + expecting a `.context` property). + +### 🛡️ Example Transformation + +#### Before (V1 Legacy) + +```typescript +import * as functions from "firebase-functions"; +export const processOrder = functions.pubsub.topic("orders").onPublish((message, context) => { + const orderId = message.json.id; + console.log(`Processing order ${orderId} at ${context.timestamp}`); +}); +``` + +#### After (V2 Target - Safe Migration) + +```typescript +import { onMessagePublished } from "firebase-functions/v2/pubsub"; +// Using direct object destructuring in the signature! +export const processOrder = onMessagePublished("orders", ({ message, context }) => { + const orderId = message.json.id; // Legacy logic remains untouched! + console.log(`Processing order ${orderId} at ${context.timestamp}`); +}); +``` + +# Verification + +After making any migration edits, immediately run the following verification +steps: + +1. Run `npm run build` to ensure the TypeScript compiler is happy with the types + and parameters. +1. Run `npm test` to verify no regressions occurred in existing unit tests. + +> [!WARNING] **Test Signature Mismatch**: The destructuring shim changes the +> function signature from two arguments `(data, context)` to a single +> destructured object `({ message, context })`. +> +> Existing V1 unit tests that invoke the function with two parameters separately +> (e.g., `myFn(mockData, mockContext)`) **will fail** because the function +> treats `mockData` as the entire event object. You will need to update test +> calls to pass a single object. +> +> **Crucial**: The key name in the test mock must match the specific **Shimmed +> Key** for that trigger (e.g., `change` for `onDocumentWritten`, `snapshot` for +> `onDocumentCreated`, `message` for PubSub, or `object` for Storage). +> +> Example: `myFn({ change: mockChange, context: mockContext })` or +> `myFn({ message: mockMessage, context: mockContext })`. See +> [signature-mapping.md](references/signature-mapping.md) for the exact keys. + +# 💸 Performance & Cost Considerations + +In Firebase Functions V2, you can handle multiple requests concurrently per +instance (up to 1,000 requests, default 80 if CPU >= 1). However, enabling +concurrency requires assigning at least 1 full CPU. + +- **V1 Cost Parity**: If you want to keep V1 fractional CPU pricing (and disable + concurrency), you must explicitly set `cpu: "gcf_gen1"`. +- **Modernization**: If you want to take advantage of Concurrency, you must + assign at least 1 CPU. + +See [configuration-migration.md](references/configuration-migration.md) for how +to set these options. + +# 🛠️ Additional V2 SDK Features + +When upgrading to V2, take advantage of modern V2 SDK features where applicable: + +- **Declarative Security (IAM Roles & APIs)**: Use `requiresRole("roles/...")` + and `requiresAPI("service.googleapis.com", "reason")` from + `firebase-functions/v2` so IAM role and Google Cloud API dependencies are + declared directly in code instead of instructing users to run manual `gcloud` + commands. +- **Lifecycle Hooks**: Use post-deployment lifecycle hooks (`afterFirstDeploy`, + `afterRedeploy`) from `firebase-functions/v2` for automated post-deployment + initialization tasks. + +# References + +- **Deep Dive into Shims**: See the architectural choices for the shim in + [destructuring-shim.md](references/destructuring-shim.md). +- **Function Name Mapping**: See the V1 vs V2 function signature mapping table + in [signature-mapping.md](references/signature-mapping.md). diff --git a/skills/firebase-v1-v2-migration/references/configuration-migration.md b/skills/firebase-v1-v2-migration/references/configuration-migration.md new file mode 100644 index 00000000..17625e34 --- /dev/null +++ b/skills/firebase-v1-v2-migration/references/configuration-migration.md @@ -0,0 +1,151 @@ +# Migrating Runtime Configurations (runWith) + +In Firebase Functions V1, you configured runtime settings like memory, timeout, +and service accounts using `.runWith()`. In V2, `.runWith()` is removed and +replaced by a more flexible options system. + +You can configure V2 functions in two ways: **Globally** (for all functions in a +file) or **Per-Function**. + +______________________________________________________________________ + +## 🌍 1. Global Configuration (`setGlobalOptions`) + +Use `setGlobalOptions` at the top of your file to set defaults for all functions +defined after it. + +### V1 Legacy + +```typescript +import * as functions from "firebase-functions"; + +export const myFn = functions + .runWith({ + memory: "1GB", + timeoutSeconds: 120, + serviceAccount: "custom-sa@my-project.iam.gserviceaccount.com", + }) + .https.onRequest((req, res) => { ... }); +``` + +### V2 Modern Equivalent + +```typescript +import { setGlobalOptions } from "firebase-functions/v2"; +import { onRequest } from "firebase-functions/v2/https"; + +// Set global defaults for this file +setGlobalOptions({ + memory: "1GiB", // Note: GiB instead of GB is preferred in V2 types + timeoutSeconds: 120, + serviceAccount: "custom-sa@my-project.iam.gserviceaccount.com", +}); + +export const myFn = onRequest((req, res) => { ... }); +``` + +______________________________________________________________________ + +## 🎯 2. Per-Function Configuration + +Pass the configuration object as the **first argument** to the V2 trigger +function. + +### V1 Legacy + +```typescript +export const processOrder = functions + .runWith({ memory: "2GB" }) + .pubsub.topic("orders") + .onPublish((message, context) => { ... }); +``` + +### V2 Modern Equivalent + +```typescript +import { onMessagePublished } from "firebase-functions/v2/pubsub"; + +export const processOrder = onMessagePublished( + { + topic: "orders", + memory: "2GiB", // Options passed as the first argument! + }, + ({ message, context }) => { ... } // Destructuring shim pattern +); +``` + +> [!TIP] **Memory Unit Recommendation**: While V1 accepted non-IEC units like +> `"1GB"` or `"2GB"`, V2 options strongly prefer IEC units like `"1GiB"`, +> `"2GiB"`, etc., to align with Cloud Run types and prevent TypeScript type +> errors. + +______________________________________________________________________ + +## ⚠️ Common Property Translations + +| V1 Property | V2 Property | Notes | +| :--------------------------- | :--------------------------- | :--------------------------------------------------------------------- | +| `memory` | `memory` | Prefer IEC units (`"1GiB"`) instead of (`"1GB"`). | +| `timeoutSeconds` | `timeoutSeconds` | Same. | +| `ingressSettings` | `ingressSettings` | Same. | +| `vpcConnector` | `vpcConnector` | Same. | +| `vpcConnectorEgressSettings` | `vpcConnectorEgressSettings` | Same. | +| `serviceAccount` | `serviceAccount` | Same. | +| `secrets` | `secrets` | Pass array of secret parameter variables, e.g. `{ secrets: [myKey] }`. | +| `failurePolicy` | `retry` | Renamed to boolean `retry: true/false` in V2 Eventarc triggers. | + +______________________________________________________________________ + +## 🔐 3. Migrating Environment Configurations (`functions.config()`) + +In V1, you used `functions.config()` to access environment configuration. In V2, +this is replaced by **Parameterized Configuration**. + +### Deterministic Rules for Migration + +Follow these rules to ensure a deterministic and safe migration: + +#### Typing + +- **Numbers**: If the value is used as a number, use `defineNumber`. +- **Secrets**: If the key contains "KEY", "SECRET", "TOKEN", or "PASSWORD", use + `defineSecret()`. + - *Note*: Secrets MUST be explicitly bound to the function that uses them in + the options object using the secret parameter variable (e.g., + `{ secrets: [myKey] }`, where `const myKey = defineSecret("MY_KEY")`). + Passing an unreferenced uppercase identifier will cause a TypeScript + reference error. +- **Lists**: Use `defineList` for comma-separated lists. +- **JSON**: Use `defineJSON` for JSON strings. +- **Buckets**: If the param is a storage bucket, set `input: 'BUCKET_PICKER'`. + +#### Initialization & Scope + +- **Global Initialization**: If a variable was initialized globally in V1 (e.g., + `const client = new Client(functions.config().key)`), you must split it to + have declaration at global scope and initialization inside `onInit`: + ```typescript + import { onInit } from "firebase-functions/v2"; + + const myKey = defineSecret("MY_KEY"); + let client: Client; + + onInit(() => { + client = new Client(myKey.value()); + }); + ``` + +#### Advanced Interpolation & Logic + +- **String Interpolation**: Use the `expr` tagged template literal from + `firebase-functions/params` (e.g., `expr`every ${period} days\`\`) instead of + standard template literals when constructing dynamic strings with parameters. + Do NOT call `.value()` inside `expr`. +- **Logic Operators**: Use expressions like + `projectID.equals('prod').thenElse(1, 0)` for logical operations instead of + ternary operators on `.value()`. + +#### Built-ins + +- Prefer built-in variables like `databaseURL`, `projectID`, `gcloudProject`, + `storageBucket` rather than defining new params for these values. diff --git a/skills/firebase-v1-v2-migration/references/destructuring-shim.md b/skills/firebase-v1-v2-migration/references/destructuring-shim.md new file mode 100644 index 00000000..6cbeb741 --- /dev/null +++ b/skills/firebase-v1-v2-migration/references/destructuring-shim.md @@ -0,0 +1,150 @@ +# Architectural Deep Dive: Destructuring Compatibility Shim + +The Destructuring Compatibility Shim is a **Zero-Touch Logic Migration** +pattern. It allows you to upgrade a function's infrastructure to V2 (and take +advantage of GCF 2nd Gen runtimes) without rewriting any of your internal +business logic. + +______________________________________________________________________ + +## 🛠️ How it Works + +When you migrate a V1 function to V2, the signature changes from two parameters +`(data, context)` to a single `CloudEvent` object. + +Instead of manually rewriting all usages of `context.params` or `message.json` +inside the function, you use JavaScript's **Object Destructuring** in the +signature. + +### Example Transformation + +#### Step 1: Legacy V1 + +```typescript +export const processOrder = functions.pubsub.topic("orders").onPublish((message, context) => { + const orderId = message.json.id; + console.log(`Processing order ${orderId} at ${context.timestamp}`); +}); +``` + +#### Step 2: Modern V2 + Shim + +We change the trigger to `onMessagePublished`, and instead of accepting `event`, +we destructure `{ message, context }` directly: + +```typescript +export const processOrder = onMessagePublished("orders", ({ message, context }) => { + const orderId = message.json.id; // Legacy logic remains untouched! + console.log(`Processing order ${orderId} at ${context.timestamp}`); +}); +``` + +### 🧠 Why This Works + +The Firebase Functions SDK uses a utility called `addV1Compat` to attach these +properties via **Lazy Getters** on the `CloudEvent` object for standard event +triggers. When you attempt to destructure `{ message, context }` from the event, +the SDK transparently maps the V2 event properties back into V1-compatible +objects on the fly! This feature is available in modern V2 environments +supported by the SDK. + +______________________________________________________________________ + +## 📖 Provider Mapping Examples + +Here are the exact destructuring patterns for every supported V2 provider: + +### 1. Cloud Firestore + +- **Created / Deleted** triggers: + ```typescript + // V2: onDocumentCreated, onDocumentDeleted + export const processDoc = onDocumentCreated("users/{id}", ({ snapshot, context }) => { ... }); + ``` +- **Updated / Written** triggers: + ```typescript + // V2: onDocumentUpdated, onDocumentWritten + export const processDoc = onDocumentUpdated("users/{id}", ({ change, context }) => { ... }); + ``` + +### 2. Cloud Storage + +- **All** triggers (`onObjectFinalized`, `onObjectDeleted`, `onObjectArchived`, + `onObjectMetadataUpdated`): + ```typescript + export const processFile = onObjectFinalized(({ object, context }) => { ... }); + ``` + +### 3. Realtime Database + +- **Created / Deleted** triggers: + ```typescript + export const processData = onValueCreated("/users/{id}", ({ snapshot, context }) => { ... }); + ``` +- **Updated / Written** triggers: + ```typescript + export const processData = onValueWritten("/users/{id}", ({ change, context }) => { ... }); + ``` + +### 4. Remote Config + +- **Updated** triggers: + ```typescript + export const processConfig = onConfigUpdated(({ version, context }) => { ... }); + ``` + +______________________________________________________________________ + +## 🛡️ Best Practices for AI Agents + +1. **Do NOT attempt to rewrite variable access inside the body.** It is + error-prone and time-consuming. +1. **Rely on the shim by default.** Only attempt a pure V2 rewrite if the user + explicitly asks for "clean-room" modernization. +1. **Always type-check (`tsc`) after the rewrite.** If the types are wrong, the + shim might not be fully supported for that specific provider yet! +1. **HTTPS Callables (Flattened Context)**: Unlike event triggers, Callables do + **not** use `V1Compat` or a `context` object. Instead, all context properties + are flattened onto the request object. + - **V1 Priority**: `(data, context) => { ... }` + - **V2 Equivalent**: `({ data, auth, app }) => { ... }` + +______________________________________________________________________ + +## 🔗 Related Migrations + +Migrating event signatures is only one part of moving from V1 to V2. Another +critical area is configuration management. + +### Parameterized Configuration + +If your functions use `functions.config()`, you should migrate to the new +**Parameterized Configuration** system in V2. The destructuring shim handles +event signatures, but it does not shim `functions.config()`. + +#### How to Migrate: + +1. **Identify Usages**: Search for `functions.config().path.to.value`. +1. **Define Parameters**: At the top of your file, define the parameter using + the appropriate primitive from `firebase-functions/params` (available types + include `defineString`, `defineSecret`, `defineInt`, `defineBoolean`, + `defineList`, and `defineJSON`): + ```typescript + import { defineString, defineSecret } from "firebase-functions/params"; + + const stripeKey = defineSecret("STRIPE_KEY"); + const apiDomain = defineString("API_DOMAIN"); + ``` +1. **Access Values**: Replace the V1 call with the `.value()` method of the + defined parameter: + - **V1**: `const key = functions.config().stripe.key;` + - **V2**: `const key = stripeKey.value();` + +> [!NOTE] When you migrate a function to V2 (even with the destructuring shim), +> `functions.config()` will return `undefined` unless you have explicitly set up +> environment variables or are running in a specific emulation mode. +> Parameterized configuration is the standard and recommended way to handle this +> in V2. + +For a complete guide and deterministic rules on how to migrate configurations, +refer to [configuration-migration.md](configuration-migration.md). diff --git a/skills/firebase-v1-v2-migration/references/signature-mapping.md b/skills/firebase-v1-v2-migration/references/signature-mapping.md new file mode 100644 index 00000000..14e8353b --- /dev/null +++ b/skills/firebase-v1-v2-migration/references/signature-mapping.md @@ -0,0 +1,88 @@ +# Firebase Functions V1 vs V2 Signature Mapping + +This reference maps legacy V1 functions to their modern V2 equivalents. It +includes the **Shimmed Parameter Key** you should use when destructuring the V2 +event object to preserve V1 business logic. + +______________________________________________________________________ + +## 🔥 Cloud Firestore + +| V1 Trigger | V2 Equivalent | Shimmed Key | Destructuring Pattern | +| :-------------------------------- | :-------------------- | :---------- | :------------------------ | +| `firestore.document().onWrite()` | `onDocumentWritten()` | `change` | `({ change, context })` | +| `firestore.document().onCreate()` | `onDocumentCreated()` | `snapshot` | `({ snapshot, context })` | +| `firestore.document().onUpdate()` | `onDocumentUpdated()` | `change` | `({ change, context })` | +| `firestore.document().onDelete()` | `onDocumentDeleted()` | `snapshot` | `({ snapshot, context })` | + +______________________________________________________________________ + +## 📨 Cloud Pub/Sub + +| V1 Trigger | V2 Equivalent | Shimmed Key | Destructuring Pattern | +| :--------------------------- | :--------------------- | :---------- | :----------------------- | +| `pubsub.topic().onPublish()` | `onMessagePublished()` | `message` | `({ message, context })` | +| `pubsub.schedule().onRun()` | `onSchedule()` | **N/A** | Access `event` directly | + +> [!NOTE] Scheduled functions moved from the `pubsub` namespace to +> `firebase-functions/v2/scheduler` +> (`import { onSchedule } from "firebase-functions/v2/scheduler"`). + +______________________________________________________________________ + +## 💾 Realtime Database + +| V1 Trigger | V2 Equivalent | Shimmed Key | Destructuring Pattern | +| :-------------------------- | :----------------- | :---------- | :------------------------ | +| `database.ref().onWrite()` | `onValueWritten()` | `change` | `({ change, context })` | +| `database.ref().onCreate()` | `onValueCreated()` | `snapshot` | `({ snapshot, context })` | +| `database.ref().onUpdate()` | `onValueUpdated()` | `change` | `({ change, context })` | +| `database.ref().onDelete()` | `onValueDeleted()` | `snapshot` | `({ snapshot, context })` | + +______________________________________________________________________ + +## 🗄️ Cloud Storage + +| V1 Trigger | V2 Equivalent | Shimmed Key | Destructuring Pattern | +| :------------------------------------ | :-------------------------- | :---------- | :---------------------- | +| `storage.object().onArchive()` | `onObjectArchived()` | `object` | `({ object, context })` | +| `storage.object().onDelete()` | `onObjectDeleted()` | `object` | `({ object, context })` | +| `storage.object().onFinalize()` | `onObjectFinalized()` | `object` | `({ object, context })` | +| `storage.object().onMetadataUpdate()` | `onObjectMetadataUpdated()` | `object` | `({ object, context })` | + +______________________________________________________________________ + +## 🌐 HTTP / Callables + +| V1 Trigger | V2 Equivalent | Shimmed Key | Destructuring Pattern | +| :------------------ | :------------------ | :---------- | :----------------------------- | +| `https.onRequest()` | `https.onRequest()` | **N/A** | Standard Express `(req, res)` | +| `https.onCall()` | `https.onCall()` | **N/A** | Destructure `({ data, auth })` | + +> [!IMPORTANT] **HTTP Callables do NOT use the Destructuring Shim.** In V2, the +> handler receives a single `CallableRequest` object (not a `CloudEvent`). You +> should destructure properties like `data`, `auth`, and `app` directly from it. +> The traditional `context` object is **unavailable**. + +______________________________________________________________________ + +## 🔑 Auth (Blocking) + +| V1 Trigger | V2 Equivalent | Shimmed Key | Destructuring Pattern | +| :--------------------------- | :------------------------------ | :---------- | :---------------------- | +| `auth.user().beforeSignIn()` | `identity.beforeUserSignedIn()` | **N/A** | Access `event` directly | +| `auth.user().beforeCreate()` | `identity.beforeUserCreated()` | **N/A** | Access `event` directly | + +> [!NOTE] Auth Blocking triggers moved to the `identity` namespace in V2. + +______________________________________________________________________ + +## ⏰ Cloud Tasks + +| V1 Trigger | V2 Equivalent | Shimmed Key | Destructuring Pattern | +| :------------------------------- | :------------------- | :---------- | :---------------------- | +| `tasks.taskQueue().onDispatch()` | `onTaskDispatched()` | **N/A** | Access `event` directly | + +> [!NOTE] Task queue triggers moved from the `tasks` namespace to +> `firebase-functions/v2/tasks` +> (`import { onTaskDispatched } from "firebase-functions/v2/tasks"`).