Идемпотентная функция cloud functions
Всем привет. Столкнулся с проблемой дубликатов при пользовании cloud functions в firebase.
Ссылаясь на их доку https://cloud.google.com/functions/docs/concepts/exec#execution_guarantees это нормальная практика, ведь гуглы заявляют о своей фишке 'at-least-once-execution'. Такое явление порождает повторное создание документов в cloud firestore

Тем самым порождая несколько ничем (кроме timestamp) неотличимых документов.
Вопрос: как можно реализовать однократное создание документа?
Сниппет при создании документа из исходного кода:
export async function addExperience(experience: IProfileAddExperienceRequest): Promise<IProfileAddExperienceResponse> {
const uId = (firebaseUser() as any).uid;
const req: any = { ...experience };
const city: ISearchLocation = req.city as ISearchLocation;
let region: any;
let country: any;
let images: string[] = [];
if (experience.images) {
for (let i = 0; i < experience.images.length; i++) {
let url: string;
url = await getImageURL(experience.images[i]);
images.push(url);
}
}
if (city && city.context) {
region = city.context.find((item: any) => item.type === 'region');
country = city.context.find((item: any) => item.type === 'country');
delete city.context;
}
if (req.description === '') {
delete req.description;
}
if (req.companyName === '') {
delete req.companyName;
}
if (req.images) {
req.images = images;
}
if (req.city) {
req.selectedCity = {
id: city.id,
name: city.name,
shortName: city.shortName,
};
delete req.city;
}
req.selectedRegion = region
? {
name: region.name,
id: region.id,
}
: city && city.type === 'region'
? city
: undefined;
req.selectedCountry = country
? {
name: country.name,
id: country.id,
}
: city && city.type === 'country'
? city
: undefined;
const ref = await CollectionService.workExperience().add({
authorid: uId,
active: true,
deleted: false,
createDate: firebase.firestore.Timestamp.now(),
...req,
});
return await ref.get();
Sourcе клауд функции:
const functions = require('firebase-functions');
const http_req = require('request');
const unirest = require('unirest');
const admin = require('firebase-admin');
admin.initializeApp();
exports.createContact = functions.firestore
.document('contacts/{contactID}').onCreate(async (snap,context) => {
snap.ref.update({
deleted:false,active:true,rate:0,createDate:new Date()
});
const db = admin.firestore();
const newContactData = snap.data();
console.log('NEW_CONTACT_DATA', newContactData);
const interestIconsDoc = await db.collection('system').doc('interest_icons').get();
const interestIconsData = interestIconsDoc.data();
const industryAvatar = interestIconsData[newContactData.selectedIndustry.id];
const userDoc = await db.collection('users').doc(newContactData.authorid).get();
const userData = userDoc.data();
console.log('USER_DATA', userData);
const activitiesData = await db
.collection('contacts')
.where('authorid', '==', newContactData.authorid)
.where('active', '==', true)
.where('deleted', '==', false)
.where('type', '==', 'activity')
.get();
const connectionsData = await db
.collection('contacts')
.where('authorid', '==', newContactData.authorid)
.where('active', '==', true)
.where('deleted', '==', false)
.where('type', '==', 'connection')
.get();
let friendsId = [];
const friendsIdDocs = await db
.collection('favorites')
.where('contactUserId', '==', newContactData.authorid)
.get();
if (friendsIdDocs.docs.length !== 0) {
friendsIdDocs.docs.forEach(doc => {
const data = doc.data();
if (!friendsId.includes(data.authorid)) {
friendsId.push(data.authorid);
}
});
}
if (friendsId.length !== 0) {
const myNewsDoc = await db
.collection('news')
.where('userData.authorid', '==', newContactData.authorid)
.get();
if (myNewsDoc.docs.length !== 0) {
myNewsDoc.docs.forEach(async (doc) => {
await db.collection('news').doc(doc.id).update({
friendsId: friendsId,
});
});
}
}
db.collection('news').add({
type: 'newActivityContact',
createDate: admin.firestore.FieldValue.serverTimestamp(),
contactId: snap.id,
friendsId: friendsId,
userData: {
authorid: newContactData.authorid,
firstname: userData.firstName && userData.firstName,
lastname: userData.lastName && userData.lastName,
convertedName: userData.convertedName && userData.convertedName,
isVip: !!userData.isVip,
avatar: userData.picUrl && userData.picUrl,
activitiesCount: activitiesData.docs && Number(activitiesData.docs.length) + newContactData.type === 'activity' ? 1 : 0, // плюсуем 1, чтобы данную карточку посчитать
connectionsCount: connectionsData.docs && Number(connectionsData.docs.length) + newContactData.type === 'connection' ? 1 : 0,
},
competencies: {
type: newContactData.type && newContactData.type,
industryIcon: industryAvatar && industryAvatar,
industry: newContactData.selectedIndustry !== undefined && newContactData.selectedIndustry.data,
branch: newContactData.selectedBranch !== undefined && newContactData.selectedBranch.data,
speciality: newContactData.selectedSpeciality !== undefined && newContactData.selectedSpeciality.data,
city: newContactData.selectedCity !== undefined && newContactData.selectedCity.name,
companyName: newContactData.companyName !== undefined && newContactData.companyName,
images: newContactData.images && newContactData.images.length ? newContactData.images: null,
description: newContactData.description ? newContactData.description : null,
}
});
const increment = admin.firestore.FieldValue.increment(1);
db.collection('system').doc('contacts').update({
counter: increment
})
});