From 55bf89ac9ae021230a2f02d05761fcfaf95832f4 Mon Sep 17 00:00:00 2001 From: besart-finsweet Date: Thu, 23 Apr 2026 16:52:59 +0200 Subject: [PATCH 1/2] feat: add generateFirestoreId utility for random Firestore document IDs --- .changeset/generate-firestore-id.md | 5 +++++ README.md | 17 +++++++++++++++++ src/id.test.ts | 24 ++++++++++++++++++++++++ src/id.ts | 28 ++++++++++++++++++++++++++++ src/index.ts | 1 + 5 files changed, 75 insertions(+) create mode 100644 .changeset/generate-firestore-id.md create mode 100644 src/id.test.ts create mode 100644 src/id.ts diff --git a/.changeset/generate-firestore-id.md b/.changeset/generate-firestore-id.md new file mode 100644 index 0000000..4b79ae0 --- /dev/null +++ b/.changeset/generate-firestore-id.md @@ -0,0 +1,5 @@ +--- +'fireworkers': minor +--- + +Add `generateFirestoreId()`, a utility that returns a random 20-character `[A-Za-z0-9]` ID in the same format Firestore uses for auto-generated document IDs. Useful when you need to know a document's ID before writing it (e.g. to reference it from sibling writes in a `batch`). Ported from `@firebase/firestore`'s `AutoId.newId()` — uses `crypto.getRandomValues` with rejection sampling to avoid modulo bias. diff --git a/README.md b/README.md index e3c491c..bb25516 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,23 @@ const response = await b.commit(); --- +### generateFirestoreId() + +Generates a random ID matching Firestore's auto-generated document ID format: 20 characters from `[A-Za-z0-9]`, produced with rejection sampling via `crypto.getRandomValues` to avoid modulo bias. Ported from [`@firebase/firestore`'s `AutoId.newId()`](https://github.com/firebase/firebase-js-sdk/blob/main/packages/firestore/src/util/misc.ts). + +Useful when you need to know a document's ID before writing it — for example, to reference it from other documents in the same [`batch`](#batchdb). + +```typescript +const id = Firestore.generateFirestoreId(); + +const b = Firestore.batch(db); +b.set(['todos', id], { title: 'Win the lottery', completed: false }); +b.set(['todo-index', id], { createdAt: Date.now() }); +await b.commit(); +``` + +--- + ## Error handling All operations reject with a `FirestoreError` when Firestore returns an error response or the network request fails. `FirestoreError` extends the built-in `Error`, so existing `try/catch` and `.message` checks keep working — but you can now branch on a stable string `code` instead of parsing the message. diff --git a/src/id.test.ts b/src/id.test.ts new file mode 100644 index 0000000..9475160 --- /dev/null +++ b/src/id.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; + +import { generateFirestoreId } from './id'; + +describe('generateFirestoreId', () => { + it('returns a 20-character string', () => { + const id = generateFirestoreId(); + expect(id).toHaveLength(20); + }); + + it('only uses characters from [A-Za-z0-9]', () => { + for (let i = 0; i < 100; i++) { + expect(generateFirestoreId()).toMatch(/^[A-Za-z0-9]{20}$/); + } + }); + + it('produces unique IDs across many invocations', () => { + const ids = new Set(); + for (let i = 0; i < 10_000; i++) { + ids.add(generateFirestoreId()); + } + expect(ids.size).toBe(10_000); + }); +}); diff --git a/src/id.ts b/src/id.ts new file mode 100644 index 0000000..421f60b --- /dev/null +++ b/src/id.ts @@ -0,0 +1,28 @@ +const AUTO_ID_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; +const AUTO_ID_LENGTH = 20; +// Largest multiple of AUTO_ID_CHARS.length that fits in a byte. +// Bytes at or above this value are discarded so the modulo below is unbiased. +const MAX_MULTIPLE = Math.floor(256 / AUTO_ID_CHARS.length) * AUTO_ID_CHARS.length; + +/** + * Generates a random ID matching Firestore's auto-generated document ID format. + * 20 characters from [A-Za-z0-9], with rejection sampling to avoid modulo bias. + * Ported from `@firebase/firestore`'s `AutoId.newId()`. + */ +export const generateFirestoreId = (): string => { + let id = ''; + + while (id.length < AUTO_ID_LENGTH) { + const bytes = new Uint8Array(40); + crypto.getRandomValues(bytes); + + for (let i = 0; i < bytes.length && id.length < AUTO_ID_LENGTH; i++) { + const byte = bytes[i]!; + if (byte < MAX_MULTIPLE) { + id += AUTO_ID_CHARS.charAt(byte % AUTO_ID_CHARS.length); + } + } + } + + return id; +}; diff --git a/src/index.ts b/src/index.ts index 3b1c187..d02bdd3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export * from './batch'; export * from './create'; export { FirestoreError, type FirestoreErrorCode } from './error'; export * from './get'; +export * from './id'; export * from './init'; export * from './query'; export * from './remove'; From e552cdae92be26609ac5703cf414d92e6a777741 Mon Sep 17 00:00:00 2001 From: besart-finsweet Date: Thu, 23 Apr 2026 18:23:31 +0200 Subject: [PATCH 2/2] feat: rename generateFirestoreId to generateDocumentId for consistency --- .changeset/generate-firestore-id.md | 2 +- README.md | 4 ++-- src/id.test.ts | 16 ++++------------ src/id.ts | 7 +++++-- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/.changeset/generate-firestore-id.md b/.changeset/generate-firestore-id.md index 4b79ae0..e9c2ed2 100644 --- a/.changeset/generate-firestore-id.md +++ b/.changeset/generate-firestore-id.md @@ -2,4 +2,4 @@ 'fireworkers': minor --- -Add `generateFirestoreId()`, a utility that returns a random 20-character `[A-Za-z0-9]` ID in the same format Firestore uses for auto-generated document IDs. Useful when you need to know a document's ID before writing it (e.g. to reference it from sibling writes in a `batch`). Ported from `@firebase/firestore`'s `AutoId.newId()` — uses `crypto.getRandomValues` with rejection sampling to avoid modulo bias. +Add `generateDocumentId()`, a utility that returns a random 20-character `[A-Za-z0-9]` ID in the same format Firestore uses for auto-generated document IDs. Useful when you need to know a document's ID before writing it (e.g. to reference it from sibling writes in a `batch`). Ported from `@firebase/firestore`'s `AutoId.newId()` — uses `crypto.getRandomValues` with rejection sampling to avoid modulo bias. diff --git a/README.md b/README.md index bb25516..56b06ee 100644 --- a/README.md +++ b/README.md @@ -339,14 +339,14 @@ const response = await b.commit(); --- -### generateFirestoreId() +### generateDocumentId() Generates a random ID matching Firestore's auto-generated document ID format: 20 characters from `[A-Za-z0-9]`, produced with rejection sampling via `crypto.getRandomValues` to avoid modulo bias. Ported from [`@firebase/firestore`'s `AutoId.newId()`](https://github.com/firebase/firebase-js-sdk/blob/main/packages/firestore/src/util/misc.ts). Useful when you need to know a document's ID before writing it — for example, to reference it from other documents in the same [`batch`](#batchdb). ```typescript -const id = Firestore.generateFirestoreId(); +const id = Firestore.generateDocumentId(); const b = Firestore.batch(db); b.set(['todos', id], { title: 'Win the lottery', completed: false }); diff --git a/src/id.test.ts b/src/id.test.ts index 9475160..395c137 100644 --- a/src/id.test.ts +++ b/src/id.test.ts @@ -1,24 +1,16 @@ import { describe, expect, it } from 'vitest'; -import { generateFirestoreId } from './id'; +import { generateDocumentId } from './id'; -describe('generateFirestoreId', () => { +describe('generateDocumentId', () => { it('returns a 20-character string', () => { - const id = generateFirestoreId(); + const id = generateDocumentId(); expect(id).toHaveLength(20); }); it('only uses characters from [A-Za-z0-9]', () => { for (let i = 0; i < 100; i++) { - expect(generateFirestoreId()).toMatch(/^[A-Za-z0-9]{20}$/); + expect(generateDocumentId()).toMatch(/^[A-Za-z0-9]{20}$/); } }); - - it('produces unique IDs across many invocations', () => { - const ids = new Set(); - for (let i = 0; i < 10_000; i++) { - ids.add(generateFirestoreId()); - } - expect(ids.size).toBe(10_000); - }); }); diff --git a/src/id.ts b/src/id.ts index 421f60b..2035e25 100644 --- a/src/id.ts +++ b/src/id.ts @@ -3,17 +3,20 @@ const AUTO_ID_LENGTH = 20; // Largest multiple of AUTO_ID_CHARS.length that fits in a byte. // Bytes at or above this value are discarded so the modulo below is unbiased. const MAX_MULTIPLE = Math.floor(256 / AUTO_ID_CHARS.length) * AUTO_ID_CHARS.length; +// Over-allocate to amortize rejected bytes: acceptance rate is ~97% (248/256), +// so 2× AUTO_ID_LENGTH almost always fills the ID in a single iteration. +const RANDOM_BYTES_PER_ITERATION = AUTO_ID_LENGTH * 2; /** * Generates a random ID matching Firestore's auto-generated document ID format. * 20 characters from [A-Za-z0-9], with rejection sampling to avoid modulo bias. * Ported from `@firebase/firestore`'s `AutoId.newId()`. */ -export const generateFirestoreId = (): string => { +export const generateDocumentId = (): string => { let id = ''; while (id.length < AUTO_ID_LENGTH) { - const bytes = new Uint8Array(40); + const bytes = new Uint8Array(RANDOM_BYTES_PER_ITERATION); crypto.getRandomValues(bytes); for (let i = 0; i < bytes.length && id.length < AUTO_ID_LENGTH; i++) {