Firestore-simple

More simple, powerful and TypeScript friendly Firestore wrapper.
- More simple API: Original Firestore only provide a slightly complicated low-level API. firestore-simple provide a simple and easy to use API.
- TypeScript friendly: firestore-simple helps you type the document. You no longer need to cast after getting a document from Firestore.
- Encoding and decoding: Convert js object <-> firestore document every time? You need define to convert function just only one time.
- Easy and safe transaction: firestore-simple allow same CRUD API in
runTransaction. No longer need to worry about transaction context.
- A lot of test: Test many of the Firestore features using REAL Firestore instead of an emulator.
Blog posts (sorry Japanese only)
NOTICE
Exported class name FirestoreSimple was DEPRECATED. Please use FirestoreSimpleAdmin instead.
You can still import FirestoreSimple, but it will be removed in future versoin.
If you use firestore-simple before v6.0.0, migrate your code like this.
import { FirestoreSimple } from 'firestore-simple'
import { FirestoreSimpleAdmin } from 'firestore-simple'
Install
npm i firestore-simple
Usage
Use with Node.js admin SDK sample.
import admin, { ServiceAccount } from 'firebase-admin'
import serviceAccount from '../../firebase_secret.json'
import { FirestoreSimpleAdmin } from 'firestore-simple'
const ROOT_PATH = 'example/usage'
admin.initializeApp({ credential: admin.credential.cert(serviceAccount as ServiceAccount) })
const firestore = admin.firestore()
interface User {
id: string,
name: string,
age: number,
}
const main = async () => {
const firestoreSimple = new FirestoreSimpleAdmin(firestore)
const dao = firestoreSimple.collection<User>({ path: `${ROOT_PATH}/user` })
const bobId = await dao.add({ name: 'bob', age: 20 })
const bob: User | undefined = await dao.fetch(bobId)
await dao.set({
id: bobId,
name: 'bob',
age: 30,
})
const deletedId = await dao.delete(bobId)
await dao.bulkSet([
{ id: '1', name: 'foo', age: 1 },
{ id: '2', name: 'bar', age: 2 },
])
const users: User[] = await dao.fetchAll()
await dao.bulkDelete(users.map((user) => user.id))
}
main()
Auto typing document data
firestore-simple automatically types document data retrieved from a collection by TypeScript generics, you need to pass type arguments when creating a FirestoreSimpleCollection object.
interface User {
id: string,
name: string,
age: number,
}
const firestoreSimple = new FirestoreSimpleAdmin(firestore)
const dao = firestoreSimple.collection<User>({ path: `user` })
After that, type of document obtained from FirestoreSimpleCollection will be User.
const bob: User | undefined = await dao.fetch(bobId)
NOTICE: The type passed to the type argument MUST have an id property. The reason is that firestore-simple treats id as firestore document id and relies on this limitation to provide a simple API(ex: fetch, set).
encode/decode
You can hook and convert object before post to firestore and after fetch from firestore.
encode is called before post, and decode is called after fetch.
It useful for common usecase, for example change property name, convert value, map to class instances and so on.
Here is example code to realize following these features.
- encode
- Map
User class each property to firestore document key/value before post
- Update
updated property using Firebase server timestamp when update document
- decode
- Map document data fetched from firestore to
User class instance
- Convert firebase timestamp object to javascript Date object
class User {
constructor(
public id: string,
public name: string,
public created: Date,
public updated?: Date,
) { }
}
const firestoreSimple = new FirestoreSimpleAdmin(firestore)
const dao = firestoreSimple.collection<User>({
path: `user`,
encode: (user) => {
return {
name: user.name,
created: user.created,
updated: admin.firestore.FieldValue.serverTimestamp()
}
},
decode: (doc) => {
return new User(
doc.id,
doc.name,
doc.created.toDate(),
doc.updated.toDate()
)
}
})
Generics of FirestoreSimpleAdmin.collection
FirestoreSimpleAdmin.collection<T, S> has two of the type arguments T and S. If property names of T and property names of the document in firestore as same, you no longer to need S. firestore-simple provide auto completion and restriction in most methods by using T.
On the other hand, if property names of the document in firestore are different from T, you need to assign S that has same property names as the document in firestore.
interface Book {
id: string,
bookTitle: string
created: Date
}
interface BookDoc {
book_title: string,
created: Date,
}
const dao = firestoreSimple.collection<Book, BookDoc>({path: collectionPath,
encode: (book) => {
return {
book_title: book.bookTitle,
created: book.created,
}
},
decode: (doc) => {
return {
id: doc.id,
bookTitle: doc.book_title,
created: doc.created.toDate(),
}
},
})
onSnapshot
firestore-simple partially supports onSnapshot. You can map raw document data to an object with decode by using toObject() .
dao.where('age', '>=', 20)
.onSnapshot((querySnapshot, toObject) => {
querySnapshot.docChanges.forEach((change) => {
if (change.type === 'added') {
const changedDoc = toObject(change.doc)
}
})
})
Subcollection
firestore-simple does not provide API that direct manipulate subcollection. But collectionFactory is useful for subcollection.
It can define encode and decode but not path. You can create AdminCollection instance from this factory with path and both encode and decode are inherited from the factory.
This is example using collectionFactory for subcollection.
interface UserFriend {
id: string,
name: string,
created: Date,
}
const userNames = ['alice', 'bob', 'john']
const main = async () => {
const firestoreSimple = new FirestoreSimpleAdmin(firestore)
const userFriendFactory = firestoreSimple.collectionFactory<UserFriend>({
decode: (doc) => {
return {
id: doc.id,
name: doc.name,
created: doc.created.toDate()
}
}
})
const users = await userDao.fetchAll()
for (const user of users) {
const userFriendDao = userFriendFactory.create(`user/${user.id}/friend`)
const friends = await userFriendDao.fetchAll()
}
CollectionGroup
Firestore collectionGroup is also supported. As same as FirestoreSimpleAdmin.collection, FirestoreSimpleAdmin.collectionGroup has generics and decode features too.
interface Review {
id: string,
userId: string,
text: string,
created: Date,
}
const firestoreSimple = new FirestoreSimpleAdmin(firestore)
const reviewCollectionGroup = firestoreSimple.collectionGroup<Review>({
collectionId: 'review',
decode: (doc) => {
return {
id: doc.id,
userId: doc.userId,
text: doc.text,
created: doc.created.toDate()
}
}
})
const reviews = await reviewCollectionGroup.fetch()
Transaction
When using runTransaction with the original firestore, some methods like get(), set() and delete() need to be called from the transaction object. This is complicated and not easy to use.
firestore-simple allows you to use the same API in transactions. This way, you don't have to change your code depending on whether inside runTransaction block or not.
interface User {
id: string,
name: string,
}
const docId = 'alice'
const collection = firestore.collection(`${ROOT_PATH}/user`)
await firestore.runTransaction(async (transaction) => {
const docRef = collection.doc(docId)
await transaction.get(docRef)
transaction.set(docRef, { name: docId })
const newDocRef = collection.doc()
transaction.set(newDocRef, { name: newDocRef.id })
})
const firestoreSimple = new FirestoreSimpleAdmin(firestore)
const dao = firestoreSimple.collection<User>({ path: `${ROOT_PATH}/user` })
await firestoreSimple.runTransaction(async (_tx) => {
await dao.fetch(docId)
await dao.set({ id: docId, name: docId })
await dao.add({ name: 'new doc' })
})
If you want to see more transaction example, please check example code and test code.
Batch
firestore-simple provides runBatch it similar to runTransaction.
set(), update(), delete() executed in the runBatch callback function are executed by batch.commit() at the end of the block. firestore-simple handles creating batch at start of runBatch and commit at end of runBatch.
interface User {
id: string,
name: string,
rank: number,
}
const userNames = ['bob', 'alice', 'john', 'meary', 'king']
const firestoreSimple = new FirestoreSimpleAdmin(firestore)
const userDao = firestoreSimple.collection<User>({ path: `${ROOT_PATH}/user` })
await firestoreSimple.runBatch(async (_batch) => {
let rank = 1
for (const name of userNames) {
await userDao.add({ name, rank })
rank += 1
}
console.dir(await userDao.fetchAll())
})
await firestoreSimple.runBatch(async (_batch) => {
let rank = 0
for (const user of users) {
if (user.rank < 4) {
await userDao.update({ id: user.id, rank })
} else {
await userDao.delete(user.id)
}
rank += 1
}
})
If you want to see more runBatch example, please check example code and test code.
If you just want to add/set/delete documents with array, you can use bulkAdd, bulkSet, bulkDelete. These are simple wrapper of batch execution.
FieldValue.increment
Firestore can increment or decrement a numeric field value. This is very useful for counter like fields.
see: https://firebase.google.com/docs/firestore/manage-data/add-data?hl=en#increment_a_numeric_value
firestore-simple supports to update a document using special value of FieldValue. So of course you can use FieldValue.increment with update.
interface User {
id: string,
coin: number,
timestamp: Date,
}
const firestoreSimple = new FirestoreSimpleAdmin(firestore)
const dao = firestoreSimple.collection<User>({ path: `${ROOT_PATH}/user` })
const userId = await dao.add({
coin: 100,
timestamp: FieldValue.serverTimestamp()
})
await dao.update({
id: userId,
coin: FieldValue.increment(100),
timestamp: FieldValue.serverTimestamp()
})
console.log(await dao.fetch(userId))
Fallback to use original firestore
Unfortunately firestore-simple does not support all the features of Firestore, so sometimes you may want to use raw collection references or document references.
In this case, you can get raw collection reference from FirestoreSimpleCollection using collectionRef also document reference using docRef(docId).
const firestoreSimple = new FirestoreSimpleAdmin(firestore)
const dao = firestoreSimple.collection<User>({ path: `user` })
const collectionRef = dao.collectionRef
const docRef = dao.docRef('documentId')
More API and example
firestore-simple provide more API and support almost firestore features.
ex: addOrSet, update, where, orderBy, limit.
You can find more example from example directory. Also test code maybe as good sample.
API document
Sorry not yet. Please check source code or look interface using your IDE.
Feature works
Contribution
Patches are welcome!
Also welcome fixing english documentation.
Development
Unit tests are using REAL Firestore(Firebase), not mock!
So if you want to run unit test in your local machine, please put your firebase secret json as firebase_secret.json in root directory.
Versioning
The versioning follows Semantic Versioning:
Given a version number MAJOR.MINOR.PATCH, increment the:
- MAJOR version when you make incompatible API changes,
- MINOR version when you add functionality in a backwards-compatible manner, and
- PATCH version when you make backwards-compatible bug fixes.
License
MIT