Firestore unique index or unique constraint?
Yes, this is possible using a combination of two collections, Firestore rules and batched writes.
https://cloud.google.com/firestore/docs/manage-data/transactions#batched-writes
The simple idea is, using a batched write, you write your document to your "data" collection and at the same write to a separate "index" collection where you index the value of the field that you want to be unique.
Using the Firestore rules, you can then ensure that the "data" collection can only have a document written to it if the document field's value also exists in the index collection and, vice versa, that the index collection can only be written to if value in the index matches what's in the data collection.
Example
Let's say that we have a User
collection and we want to ensure that the username
field is unique.
Our User
collection will contain simply the username
/User/{id}
{
username: String
}
Our Index
collection will contain the username in the path and a value
property that contains the id of the User that is indexed.
/Index/User/username/{username}
{
value: User.id
}
To create our User
we use a batch write to create both the User
document and the Index
document at the same time.
const firebaseApp = ...construct your firebase app
const createUser = async (username) => {
const database = firebaseApp.firestore()
const batch = database.batch()
const Collection = database.collection('User')
const ref = Collection.doc()
batch.set(ref, {
username
})
const Index = database.collection('Index')
const indexRef = Index.doc(`User/username/${username}`)
batch.set(indexRef, {
value: ref.id
})
await batch.commit()
}
To update our User
's username we use a batch write to update the User
document, delete the previous Index
document and create a new Index
document all at the same time.
const firebaseApp = ...construct your firebase app
const updateUser = async (id, username) => {
const database = firebaseApp.firestore()
const batch = database.batch()
const Collection = database.collection('User')
const ref = Collection.doc(id)
const refDoc = await ref.get()
const prevData = refDoc.data()
batch.update(ref, {
username
})
const Index = database.collection('Index')
const prevIndexRef = Index.doc(`User/username/${prevData.username}`)
const indexRef = Index.doc(`User/username/${username}`)
batch.delete(prevIndexRef)
batch.set(indexRef, {
value: ref.id
})
await batch.commit()
}
To delete a User
we use a batch write to delete both the User
document and the Index
document at the same time.
const firebaseApp = ...construct your firebase app
const deleteUser = async (id) => {
const database = firebaseApp.firestore()
const batch = database.batch()
const Collection = database.collection('User')
const ref = Collection.doc(id)
const refDoc = await ref.get()
const prevData = refDoc.data()
batch.delete(ref)
const Index = database.collection('Index')
const indexRef = Index.doc(`User/username/${prevData.username}`)
batch.delete(indexRef)
await batch.commit()
}
We then setup our Firestore rules so that they only allow a User
to be created if the username is not already indexed for a different User
. A User
's username can only be updated if an Index
does not already exist for the username and a User
can only be deleted if the Index
is deleted as well. Create and update will fail with a "Missing or insufficient permissions" error if a User
with the same username
already exists.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Index collection helper methods
function getIndexAfter(path) {
return getAfter(/databases/$(database)/documents/Index/$(path))
}
function getIndexBefore(path) {
return get(/databases/$(database)/documents/Index/$(path))
}
function indexExistsAfter(path) {
return existsAfter(/databases/$(database)/documents/Index/$(path))
}
function indexExistsBefore(path) {
return exists(/databases/$(database)/documents/Index/$(path))
}
// User collection helper methods
function getUserAfter(id) {
return getAfter(/databases/$(database)/documents/User/$(id))
}
function getUserBefore(id) {
return get(/databases/$(database)/documents/User/$(id))
}
function userExistsAfter(id) {
return existsAfter(/databases/$(database)/documents/User/$(id))
}
match /User/{id} {
allow read: true;
allow create: if
getIndexAfter(/User/username/$(getUserAfter(id).data.username)).data.value == id;
allow update: if
getIndexAfter(/User/username/$(getUserAfter(id).data.username)).data.value == id &&
!indexExistsBefore(/User/username/$(getUserAfter(id).data.username));
allow delete: if
!indexExistsAfter(/User/username/$(getUserBefore(id).data.username));
}
match /Index/User/username/{username} {
allow read: if true;
allow create: if
getUserAfter(getIndexAfter(/User/username/$(username)).data.value).data.username == username;
allow delete: if
!userExistsAfter(getIndexBefore(/User/username/$(username)).data.value) ||
getUserAfter(getIndexBefore(/User/username/$(username)).data.value).data.username != username;
}
}
}
[Its not a perfect solution but working]
I have done this unique key using key... I want my table to be having unique date value. so i made it key of my document. Any way i am able to get all documents
db.collection('sensors').doc(sensorId).collection("data").doc(date).set(dataObj).then(() => {
response.send(dataObj);
});
What about doing a Transaction to first check if there are documents with the same value in this unique field, and only create the document if the result is empty.
As an example, creating a User with username as unique field:
type User = {
id?: string
username: string
firstName: string
lastName: string
}
async function createUser(user: User) {
try {
const newDocRef = db.collection('Users').doc()
await db.runTransaction(async t => {
const checkRef = db.collection('Users')
.where('username', '==', user.username)
const doc = await t.get(checkRef)
if (!doc.empty) {
throw new FirebaseError('firestore/unique-restriction',
`There is already a user with the username: '${user.username}' in the database.`
)
}
await t.create(newDocRef, user)
})
console.log('User Created')
} catch (err) {
if (err instanceof FirebaseError) {
console.log('Some error in firebase')
//Do something
} else {
console.log('Another error')
//Do whatever
}
}
}
Is this code ok or am I missing something?.