Data Operations

Since v2.1.0

Custom Scripts

Write small JavaScript snippets that run inside the app to compute values in bulk updates, to reshape documents during export and import, and to transform Firebase Authentication users during users export and import. Scripts can query the database, call external APIs and generate IDs — with clear limits and a per-job error policy.

What it is

Some transformations cannot be expressed with a fixed value or a built-in conversion: joining data from another collection, calling an external API, normalizing a value with your own rules. For those cases Fuego lets you write a custom script — a small piece of JavaScript executed by an embedded engine inside the app, once per document.

Custom scripts are available in five flows:

FlowWhereThe script returns
Update documentsThe Custom script value type, and Use custom function in Convert valueThe new value for the target field
Export collectionThe Transform script panelThe object written to the file
Import collectionThe Transform script panelThe data to import
Export usersThe Transform script panelThe object written to the file
Import usersThe Transform script panelThe row to import

In every flow, returning undefined means “do nothing here”: leave the document unchanged (update), omit the document or user (export), or skip the row (import).

Writing a script

Scripts are written as a body: statements and a top-level return, no function wrapper needed. await is allowed.

const data = doc.data();
if (!data.email) return FieldValue.delete();
return data.email.trim().toLowerCase();

Two rules to keep in mind:

  • Only the async APIs provided by Fuego (db, database(), fetch) actually resolve. setTimeout does not exist, and awaiting any other promise source fails the script with “script did not resolve synchronously”.
  • Each invocation has a 30 second timeout.

The editor is a full Monaco editor with autocomplete for every injected global, and syntax errors are checked live by the same engine that runs the job.

You are not left guessing what is available:

  • Every script editor starts with a foldable comment header documenting the globals of that flow, with examples — fold it away once you know the API.
  • The question-mark button above the editor opens the Script reference dialog, listing the data available in this script, the available functions and complete examples.
  • The folder button opens the Manage scripts dialog (see Script library below).

The Test button at the bottom of every editor runs the script against real data without writing anything — see Testing a script below.

Testing a script

Every script editor — inline and fullscreen — has a Test button at the bottom. It opens a dialog that runs the script against the first documents the job would process: up to 10, or fewer if the query has a smaller limit. For the import flow, the test runs against the first rows of the import file, parsed exactly like the real import — including CSV “skip conversion” columns — and empty rows are reported as errors, just as in the real job.

Each row of the result shows the document (or row) path and one of:

  • the script’s output, rendered as JSON;
  • a Skipped tag, when the script returned undefined;
  • the error message, when the script failed on that document.

Database writes are always disabled during a test, so a test never touches your data. A re-run button executes the test again after you edit the script.

For exports the test shapes data exactly like the real export before running the script: with Anonymize data enabled it masks the values (scripts never see unmasked ones), and with Include all attributes off it narrows the row to the picked attributes. The preview therefore matches the exported file.

In the users flows the test works the same way: the users export test runs against the first 10 users of the selected tenant, with each row shaped by the current export options; the users import test runs against the first 10 rows of the file, and also shows the error the import would report for each transformed row. Writes stay disabled here too.

Globals per flow

Each flow injects its own inputs on top of the common globals:

FlowGlobals
Update — Custom scriptdoc (the document), field (the target field path)
Update — Use custom functionvalue (the current field value), plus doc and field
Export — Transform scriptdoc (the whole document, whatever the attribute selection), data (the row the export options produce: masked when anonymization is on, narrowed to the picked attributes)
Import — Transform scriptdocId, collection, data (the raw file row, including Fuego entity values such as {"__time__": …})
Users export — Transform scriptuser (the Firebase Auth user record), data (the row as the export options serialize it)
Users import — Transform scriptuid (the user id read from the row, may be empty), data (the raw file row)

doc behaves like a Firestore DocumentSnapshot: doc.id, doc.exists, doc.ref, doc.data(), doc.get('a.b'), and doc.createTime / doc.updateTime / doc.readTime as Date.

Firebase Authentication users

The same transform scripts are available in the users export and import jobs: enable Transform each user with a custom script in the Export users or Import users dialog.

Users export

The script runs once per user and receives:

  • user — the Firebase Auth user record, in the same shape as the Node Admin SDK’s UserRecord: uid, email, emailVerified, displayName, photoURL, phoneNumber, disabled, tenantId, customClaims, providerData, passwordHash, passwordSalt, and metadata.creationTime / metadata.lastSignInTime / metadata.lastRefreshTime as Date values.
  • data — the row exactly as the export options serialize it (format, included columns, timestamp format).

Return the object to write to the file; return undefined to omit the user from the export.

// Only active users, with a claim-derived column
if (user.disabled) return undefined;
return { ...data, admin: !!get(user, 'customClaims.admin') };

Users import

The script runs on each file row before the row is parsed into a user, and receives:

  • uid — the user id read from the row (an empty string when the row has none).
  • data — the raw file row, with the same field names as the file: uid, email, emailVerified, customClaims, __passwordHash__, __creationTime__, and so on.

Because the script runs before parsing, the returned object uses the same field vocabulary as the file. Return the row to import; return undefined to skip it. The import preview runs the script too, so the preview numbers already reflect skipped and transformed rows.

// Normalize emails and skip rows without one
if (!data.email) return undefined;
return { ...data, email: data.email.trim().toLowerCase() };

Everything else on this page applies to the users flows as well: the same editor (starter template, autocomplete, Script reference dialog, fullscreen), the script library — with the dedicated Users export and Users import contexts — the db / database() and fetch access, the helpers, and the Test button.

Common globals

These are available in every script, in every flow.

db — the job’s database

A Node-Admin-SDK-like surface over the database the job runs on:

// Queries
const snap = await db
  .collection('users')
  .where('age', '>=', 18)
  .orderBy('name')
  .limit(10)
  .select('name', 'email')
  .get(); // QuerySnapshot: .docs, .empty, .size, .forEach

const one = await db.doc('users/u1').get(); // DocumentSnapshot
const group = await db.collectionGroup('posts').where('draft', '==', false).get();

// Writes
await db.collection('logs').add({ at: new Date() });
await db.doc('users/u1').set({ active: true }, { merge: true });
await db.doc('users/u1').update({ 'stats.visits': 10 });
await db.doc('users/u1').delete();
  • Queries without a limit() are capped at 1000 documents.
  • Writes respect the database’s read-only flag and refuse anonymized data.
  • There are no batches, transactions or count() aggregations yet.

database(id) — another database

Returns the same surface as db for another database of the same project:

const staging = database('staging');
const snap = await staging.collection('users').get();

fetch(url, options) — HTTP requests

const res = await fetch('https://api.example.com/rates', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ currency: 'EUR' }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const rates = await res.json(); // also: await res.text(), res.headers.get('x-…')

Response bodies are capped at 20 MB.

util — ID and number helpers

FunctionReturns
util.uuid()UUID v4
util.uuidv7()UUID v7 (time sorted)
util.ksuid()KSUID (K-sortable unique ID)
util.nanoid()NanoID
util.firestoreId()A Firestore-style random document ID
util.randomNumber(min, max)A random number in the given range

get, pick, omit — object helpers

Lodash-like helpers for reshaping plain objects, handy in every flow:

get(data, 'profile.full_name', '');              // safe deep read with a default
return pick(data, 'title', 'owner.email');       // keep only the listed paths
return { ...omit(data, 'internalNotes'), exportedAt: new Date() };
  • get(object, path, defaultValue) supports lodash-style paths such as 'a.b[0].c'.
  • pick(object, ...paths) and omit(object, ...paths) support deep paths too, and also accept arrays of paths.
  • omit returns a copy of the object; class values it does not touch (Date, GeoPoint, DocumentReference, …) are left intact.

Firestore value classes

  • GeoPoint(lat, lng) — build a geopoint.
  • VectorValue — the class vector fields arrive as.
  • FieldValueFieldValue.delete() and FieldValue.serverTimestamp() are supported return values in the update flow.

How values map to JavaScript

Firestore values arrive in the script as their natural JavaScript counterpart:

In FirestoreIn the script
TimestampDate
Integernumber; values beyond ±2^53 arrive as BigInt
Doublenumber
GeopointGeoPoint
ReferenceDocumentReference
BytesUint8Array
VectorVectorValue

When writing back, whole JavaScript numbers are written as integers — unless the stored field is a double, in which case the type is preserved on update.

Limits

LimitValue
Script timeout30 seconds per invocation
Queries without limit()Capped at 1000 documents
fetch response body20 MB
TimersNo setTimeout; only the provided async APIs resolve
Database featuresNo batches, transactions or count() yet
WritesRespect the read-only flag; refuse anonymized data

Error policy

Every job with a script has an If the script fails on a document setting:

  • Skip the document and continue (default) — failures are counted, and the job details list every failed document with its path and the error message.
  • Stop the job — the first failure aborts the whole job.

The users export and import jobs have the same setting, worded per user: If the script fails on a user, with Skip the user and continue or Stop the job.

Script library

Scripts can be saved and reused across jobs:

  • Save to library stores the current script under a name.
  • Load script… opens the library from any of the script dialogs; the picker only shows scripts saved for the same flow.

Manage scripts

The Manage scripts dialog — opened with the folder button above any script editor — lists every saved script with its name, its context (the flow it was saved for), its description and when it was last modified. From there you can view a script, edit it — name, description, context and code, with the same live syntax validation as the job editors — or delete it.

Synchronization

Saved scripts are included in settings synchronization. They are global, not project-scoped, so they travel with every synced project, with the same conflict handling as queries and bookmarks.