Search code examples
typescriptfirebasegoogle-cloud-firestoregoogle-cloud-functionsfirebase-cli

`setDoc` write to Firestore from TypeScript Functions messes up directory structure on `npm run build`


Transpiling TypeScript creates a new, incorrect directory structure when a Firebase Function writes to Firestore with setDoc or updateDoc.

This code transpiles and runs correctly:

index.ts

import * as functions from "firebase-functions";
export const MakeUppercase = functions.firestore.document('Messages/{docId}').onCreate((snap, context) => {
    try {
      const original = snap.data().original;
      const uppercase = original.toUpperCase();
      return snap.ref.set({ uppercase }, { merge: true }); // writes to the same document
    } catch (error) {
      console.error(error); // emulator always throws an "unhandled error": "Your function timed out after ~60s."
      return 0;
    }
  });

The first time the function is transpiled in a new directory the transpiler makes this directory structure:

myproject
├── environments
│   └── environment.ts
├── functions
│   ├── lib
│   │   ├── index.js
│   │   ├── index.js.map
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── src
│   │   ├── index.ts
│   └── tsconfig.json

That directory structure is correct. The main in package.json is

lib/index.js

Let's change the code a little and use setDoc to write to a different directory in Firestore:

index.ts

import * as functions from "firebase-functions";
import { initializeApp } from "firebase/app";
import { getFirestore, setDoc, doc } from "firebase/firestore";
import { environment } from '../../environments/environment';
const firebaseApp = initializeApp(environment.firebase);
const firestore = getFirestore(firebaseApp);

export const MakeUppercase = functions.firestore.document('Messages/{docId}').onCreate((snap, context) => {
    try {
      const original = snap.data().original;
      const uppercase = original.toUpperCase();

      return setDoc(
        doc(firestore, 'AnotherCollection', context.params.docId),
        { uppercase }, { merge: true }
      );

    } catch (error) {
      console.error(error); // emulator always throws an "unhandled error": "Your function timed out after ~60s."
      return 0;
    }
  });

I transpile the function with npm run build in the functions directory. The directory structure changes to:

myproject
├── environments
│   └── environment.ts
├── functions
│   ├── lib
│   │   ├── environments
│   │   │   ├── environment.js
│   │   │   └── environment.js.map
│   │   ├── functions
│   │   │   └── src
│   │   │       ├── index.js
│   │   │       ├── index.js.map
│   │   ├── index.js
│   │   ├── index.js.map
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── src
│   │   ├── index.ts
│   └── tsconfig.json

The transpiler added three new directories and four new files. It's exposing my apiKey and other credentials. To make the new function run I changed main to

lib/functions/src/index.js

Now the new code runs but it won't write to Firestore. I'm getting PERMISSION_DENIED: Missing or insufficient permissions. errors. Here's my rules:

firestore-rules

rules_version = "2";
service cloud.firestore {
  match /databases/{database}/documents {
  
    match /{document=**} {
      allow read, write;
    }
  }
}

Don't worry, those rules are for the emulator, not for production.

A couple more weird things. Visual Studio Code shows a single directory functions/src, not two, nested directories. (MacOS shows two nested directories.)

I can deploy the new function to Cloud Functions with firebase deploy but gcloud functions deploy won't deploy the function. It throws an error that it can't find index.js:

lib/functions/src/index.js does not exist;

What is going on here??? To test this I spun up four new Firebase Functions directories. I tried different functions. It happens every time I use setDoc or updateDoc, and doesn't happen if the code doesn't include setDoc or updateDoc.

Here's my tsconfig.json:

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "outDir": "lib",
    "sourceMap": true,
    "strict": true,
    "target": "es2017"
  },
  "compileOnSave": true,
  "include": [
    "src"
  ]
}

Solution

  • Thanks, @Doug Stevenson! What I'm reading in your comment is that Firebase Cloud Functions can't use the Firebase 9 syntax setDoc and updateDoc for Firestore, and uploadString etc. for Storage. We have to use the old school Firebase syntax, like this:

    index.js

    const functions = require('firebase-functions');
    const admin = require('firebase-admin');
    admin.initializeApp();
    
    export const MakeUppercase = functions.firestore.document('Messages/{docId}').onCreate(async (snap: any, context: any) => {
        try {
            const original = snap.data().original;
            const uppercase = original.toUpperCase();
            await admin.firestore().collection('AnotherCollection').add({uppercase: uppercase});
            return uppercase;
        } catch (error) {
            console.error(error); // emulator always throws an "unhandled error": "Your function timed out after ~60s."
            return 0;
        }
    });
    

    That transpiled without creating new directories or files, and ran without a problem.