I have a firestore collection called Posts I make an insert on the client side and it works.
I want to add the createdAt and updatedAt fields to every insert in my posts collection firestore using firebase functions.
I have a firestore collection called Posts I make an insert on the client side and it works.
I want to add the createdAt and updatedAt fields to every insert in my posts collection firestore using firebase functions.
UPDATE 3/5/23 - See my blog post for updates on this:
https://code.build/p/FNqgCcgVsGFrHCRjjf4vg/handling-firestore-timestamps
UPDATE 1/31/21 - While I believe my package is great code and answers the question, there is a cheaper way of doing this: firestore rules:
allow create: if request.time == request.resource.data.createdAt;
allow update: if request.time == request.resource.data.updatedAt;
If the updatedAt or createdAt are not added on the front end with the correct date and time, it will not allow the update / create. This is much cheaper as it does not require a function for data, nor an extra write everytime you update something.
Do not use a regular date field, be sure to add the timestamp on the frontend via:
firebase.firestore.FieldValue.serverTimestamp;
I created a universal cloud function to update whatever documents you want with the createdAt and updatedAt timestamp:
exports.myFunction = functions.firestore
.document('{colId}/{docId}')
.onWrite(async (change, context) => {
// the collections you want to trigger
const setCols = ['posts', 'reviews','comments'];
// if not one of the set columns
if (setCols.indexOf(context.params.colId) === -1) {
return null;
}
// simplify event types
const createDoc = change.after.exists && !change.before.exists;
const updateDoc = change.before.exists && change.after.exists;
const deleteDoc = change.before.exists && !change.after.exists;
if (deleteDoc) {
return null;
}
// simplify input data
const after: any = change.after.exists ? change.after.data() : null;
const before: any = change.before.exists ? change.before.data() : null;
// prevent update loops from triggers
const canUpdate = () => {
// if update trigger
if (before.updatedAt && after.updatedAt) {
if (after.updatedAt._seconds !== before.updatedAt._seconds) {
return false;
}
}
// if create trigger
if (!before.createdAt && after.createdAt) {
return false;
}
return true;
}
// add createdAt
if (createDoc) {
return change.after.ref.set({
createdAt: admin.firestore.FieldValue.serverTimestamp()
}, { merge: true })
.catch((e: any) => {
console.log(e);
return false;
});
}
// add updatedAt
if (updateDoc && canUpdate()) {
return change.after.ref.set({
updatedAt: admin.firestore.FieldValue.serverTimestamp()
}, { merge: true })
.catch((e: any) => {
console.log(e);
return false;
});
}
return null;
});
createdAt
in each write, wouldn't you need to either know definitively that the document doesn't exist yet or already have the current value on the client? Given that both of those would require a database read (which you'd have to do before every write), it seems like it'd be cheaper to use the function method for createdAt
and the rules method for updatedAt
. Or am I missing something? β
Socle createDoc
to see if the doc exists. I would still just use rules for both, as it is much much cleaner. β
Levulose allow create: if request.time == request.resource.data.createdAt; allow update: if request.time == request.resource.data.updatedAt;
is not adding createdAt server side. It is stopping create. Why do you include that reference? β
Unable serverTimestamp()
β
Levulose In order to add a createdAt
timestamp to a Post
record via a Cloud Function, do as follows:
exports.postsCreatedDate = functions.firestore
.document('Posts/{postId}')
.onCreate((snap, context) => {
return snap.ref.set(
{
createdAt: admin.firestore.FieldValue.serverTimestamp()
},
{ merge: true }
);
});
In order to add a modifiedAt
timestamp to an existing Post
you could use the following code. HOWEVER, this Cloud Function will be triggered each time a field of the Post document changes, including changes to the createdAt
and to the updatedAt
fields, ending with an infinite loop....
exports.postsUpdatedDate = functions.firestore
.document('Posts/{postId}')
.onUpdate((change, context) => {
return change.after.ref.set(
{
updatedAt: admin.firestore.FieldValue.serverTimestamp()
},
{ merge: true }
);
});
So you need to compare the two states of the document (i.e. change.before.data()
and change.after.data()
to detect if the change is concerning a field that is not createdAt
or updatedAt
.
For example, imagine your Post document only contains one field name
(not taking into account the two timestamp fields), you could do as follows:
exports.postsUpdatedDate = functions.firestore
.document('Posts/{postId}')
.onUpdate((change, context) => {
const newValue = change.after.data();
const previousValue = change.before.data();
if (newValue.name !== previousValue.name) {
return change.after.ref.set(
{
updatedAt: admin.firestore.FieldValue.serverTimestamp()
},
{ merge: true }
);
} else {
return false;
}
});
In other words, I'm afraid you have to compare the two document states field by field....
createdAt: snap.createTime
instead. FieldValue.serverTimestamp()
has an issue in that it sets the timestamp based on the invocation of the onCreate
function, which can be a few hundred or thousand milliseconds later. β
Hosiery This is what I have used to prevent the firebase firestore infinite loop.
I prefer to put the logic in a onWrite
compared to onUpdate
trigger
I use the npm package fast-deep-equal
to compare changes between incoming and previous data.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
const equal = require('fast-deep-equal/es6');
export const notificationUpdated = functions.firestore
.document('notifications/{notificationId}')
.onWrite((change, context) => {
// Get an object with the current document value.
// If the document does not exist, it has been deleted.
const document = change.after.exists ? change.after.data() : null;
// Get an object with the previous document value (for update or delete)
const oldDocument = change.before.data();
if (document && !change.before.exists) {
// This is a new document
return change.after.ref.set(
{
createdAt: admin.firestore.FieldValue.serverTimestamp(),
updatedAt: admin.firestore.FieldValue.serverTimestamp()
},
{ merge: true }
);
} else if (document && change.before.exists) {
// This is an update
// Let's check if it's only the time that has changed.
// I'll do this by making updatedAt a constant, then use `fast-deep-equal` to compare the rest
const onlyTimeChanged = equal({ ...oldDocument, updatedAt: 0 }, { ...document, updatedAt: 0 });
console.log(`Only time changed? ${onlyTimeChanged}`);
if (onlyTimeChanged) {
// The document has just been updated.
// Prevents an infinite loop
console.log('Only time has changed. Aborting...');
return false;
}
return change.after.ref.set(
{
updatedAt: admin.firestore.FieldValue.serverTimestamp()
},
{ merge: true }
);
} else if (!document && change.before.exists) {
// This is a doc delete
// Log or handle it accordingly
return false;
} else {
return false;
}
});
Hope this helps
const after = change.after.data();
const before = change.before.data();
const check = Object.keys(after).filter(key => (key !== 'createdAt') && (key !== 'updatedAt')).map(key => after[key] != before[key]);
if (check.includes(true)) {
return change.after.ref.set(
{
updatedAt: admin.firestore.FieldValue.serverTimestamp()
},
{ merge: true }
);
} else {
return false;
}
This solution supports first-level subcollections and is based off @Jonathan's answer above:
**
* writes fields common to root-level collection records that are generated by the
* admin SDK (backend):
* - createdAt (timestamp)
* - updatedAt (timestamp)
*/
exports.createCommonFields = functions.firestore
.document('{colId}/{docId}')
.onWrite(async (change, context) => {
// the collections you want to trigger
const setCols = ['posts', 'reviews', 'comments', ];
// run the field creator if the document being touched belongs to a registered collection
if (setCols.includes(context.params.colId)) {
console.log(`collection ${context.params.colId} is not registered for this trigger`);
return null;
} else {
console.log(`running createCommonFields() for collection: ${context.params.colId}`);
}
// cause the creation of timestamp fields only
_createCommonFields(change);
});
/**
* createCommonFields' equivalent for sub-collection records
*/
exports.createCommonFieldsSubColl = functions.firestore
.document('{colId}/{colDocId}/{subColId}/{subColDocId}')
.onWrite(async (change, context) => {
console.log(`collection: ${context.params.colId}, subcollection: ${context.params.subColId}`);
// the subcollections of the collections you want to trigger
// triggers for documents like 'posts/postId/versions/versionId, etc
const setCols = {
'posts': ['versions', 'tags', 'links', ],
'reviews': ['authors', 'versions'],
'comments': ['upvotes', 'flags'],
};
// parse the collection and subcollection names of this document
const colId = context.params.colId;
const subColId = context.params.subColId;
// check that the document being triggered belongs to a registered subcollection
// e.g posts/versions; skip the field creation if it's not included
if (setCols[colId] && setCols[colId].includes(subColId)) {
console.log(`running createCommonFieldsSubColl() for this subcollection`);
} else {
console.log(`collection ${context.params.colId}/${context.params.subColId} is not registered for this trigger`);
return null;
}
// cause the creation of timestamp fields
_createCommonFields(change);
});
/**
* performs actual creation of fields that are common to the
* registered collection being written
* @param {QueryDocumentSnapshot} change a snapshot for the collection being written
*/
async function _createCommonFields(change) {
// simplify event types
const createDoc = change.after.exists && !change.before.exists;
const updateDoc = change.before.exists && change.after.exists;
const deleteDoc = change.before.exists && !change.after.exists;
if (deleteDoc) {
return null;
}
// simplify input data
const after = change.after.exists ? change.after.data() : null;
const before = change.before.exists ? change.before.data() : null;
// prevent update loops from triggers
const canUpdate = () => {
// if update trigger
if (before.updatedAt && after.updatedAt) {
if (after.updatedAt._seconds !== before.updatedAt._seconds) {
return false;
}
}
// if create trigger
if (!before.createdAt && after.createdAt) {
return false;
}
return true;
}
const currentTime = admin.firestore.FieldValue.serverTimestamp();
// add createdAt
if (createDoc) {
return change.after.ref.set({
createdAt: currentTime,
updatedAt: currentTime,
}, { merge: true })
.catch((e) => {
console.log(e);
return false;
});
}
// add updatedAt
if (updateDoc && canUpdate()) {
return change.after.ref.set({
updatedAt: currentTime,
}, { merge: true })
.catch((e) => {
console.log(e);
return false;
});
}
return null;
}
You do not need Cloud Functions to do that. It is much simpler (and cheaper) to set server timestamp in client code as follows:
var timestamp = firebase.firestore.FieldValue.serverTimestamp()
post.createdAt = timestamp
post.updatedAt = timestamp
allow create: if request.time == request.resource.data.createdAt;
β
Deiform © 2022 - 2024 β McMap. All rights reserved.