-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feat/campsite-timestamps'
- Loading branch information
Showing
7 changed files
with
164 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1 @@ | ||
# called from ./api | ||
node_modules/.bin/mocha -w | ||
node_modules/.bin/mocha -w -f onCampsitesWrite |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,62 @@ | ||
const { Timestamp } = require('firebase-admin/firestore'); | ||
const { replicate } = require('./shared'); | ||
|
||
/** | ||
* @param {import("firebase-functions").Change<any>} change | ||
* @param {import("firebase-functions").Change<import('@google-cloud/firestore').DocumentSnapshot<import('../campsites').Garden>>} change | ||
*/ | ||
module.exports = async (change) => { | ||
// First replicate the change | ||
await replicate({ | ||
change, | ||
tableName: 'campsites', | ||
// Omit legacy props | ||
pick: ['description', 'location', 'facilities', 'listed', 'createTime', 'updateTime', 'photo'] | ||
pick: [ | ||
'description', | ||
'location', | ||
'facilities', | ||
'listed', | ||
'createTime', | ||
'updateTime', | ||
'photo', | ||
'latestListedChangeAt', | ||
'latestRemovedAt', | ||
'latestWarningForInactivityAt' | ||
] | ||
}); | ||
// | ||
// Prepare input for change detection | ||
const { before, after } = change; | ||
let beforeData = null; | ||
let afterData = null; | ||
if (before.exists) { | ||
beforeData = before.data(); | ||
} | ||
if (after.exists) { | ||
afterData = after.data(); | ||
} | ||
const isCreation = !before.exists; | ||
const isDeletion = !after.exists; | ||
const listedChanged = beforeData?.listed !== afterData?.listed; | ||
|
||
// Next, update the listed timestamp in the Firebase doc, but only if it was a document update, | ||
// and only if the listed property changed. | ||
if (isCreation || isDeletion || !listedChanged) { | ||
return; | ||
} | ||
|
||
let latestListedChangeAt; | ||
// Did the removal date also change to a defined value? Then use that value for the last change date. | ||
// We guarantee these properties to be equal in this case. | ||
if ( | ||
beforeData?.latestRemovedAt !== afterData?.latestRemovedAt && | ||
afterData?.latestRemovedAt instanceof Timestamp | ||
) { | ||
latestListedChangeAt = afterData.latestRemovedAt; | ||
} else { | ||
latestListedChangeAt = Timestamp.now(); | ||
} | ||
|
||
// Update the document | ||
// This should result in a new listener call that will just replicate. | ||
await after.ref.update({ latestListedChangeAt }); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
const assert = require('node:assert'); | ||
const { Timestamp } = require('firebase-admin/firestore'); | ||
const { db } = require('../seeders/app'); | ||
const { clearAuth, clearFirestore, wait } = require('./util'); | ||
const { createNewUser, createGarden } = require('../seeders/util'); | ||
|
||
// Note: this behavior requires replication to be "on" | ||
describe('onCampsitesWrite', () => { | ||
let user1; | ||
let campsiteDocRef; | ||
|
||
const waitForTriggersTimeout = 5000; | ||
|
||
beforeEach(async () => { | ||
// Seed a single test user | ||
user1 = await createNewUser( | ||
{ email: 'user1@slowby.travel' }, | ||
{ firstName: 'Bob', lastName: 'Dylan', countryCode: 'US' } | ||
).then((user) => | ||
createGarden( | ||
{ | ||
latitude: 50.952798579681854, | ||
longitude: 4.763172541851901 | ||
}, | ||
user, | ||
{ | ||
description: | ||
'Hello, this is a test garden. If you want to stay here, please send an SMS to 0679669739 or 0681483065.' | ||
} | ||
) | ||
); | ||
campsiteDocRef = db.collection('campsites').doc(user1.uid); | ||
}); | ||
|
||
const totalTimeout = waitForTriggersTimeout + 2000; | ||
|
||
it('auto-updates the listed change time when the user unlists or relists their garden', async () => { | ||
// No listed change should be defined, yet | ||
assert(typeof (await (await campsiteDocRef.get()).data().latestListedChangeAt) === 'undefined'); | ||
|
||
// Unlist the garden | ||
await campsiteDocRef.update({ listed: false }); | ||
|
||
// Wait for firestore triggers to pass | ||
await wait(waitForTriggersTimeout); | ||
|
||
const { latestListedChangeAt } = (await campsiteDocRef.get()).data(); | ||
|
||
// Check if a date was set | ||
assert(latestListedChangeAt instanceof Timestamp); | ||
|
||
// Re-list | ||
await campsiteDocRef.update({ listed: true }); | ||
await wait(waitForTriggersTimeout); | ||
|
||
// Check if updated again | ||
const { latestListedChangeAt: secondTimestamp } = (await campsiteDocRef.get()).data(); | ||
assert(latestListedChangeAt.valueOf() < secondTimestamp.valueOf()); | ||
}).timeout(totalTimeout * 2); | ||
|
||
it('has a listed change time exactly equal to the removal time, when manually unlisted', async () => { | ||
// Remove by force | ||
await campsiteDocRef.update({ latestRemovedAt: Timestamp.now(), listed: false }); | ||
|
||
await wait(waitForTriggersTimeout); | ||
|
||
// Check equality | ||
const data = /** @type{import('../../src/lib/types/Garden').Garden} */ ( | ||
(await campsiteDocRef.get()).data() | ||
); | ||
assert(data.latestRemovedAt.valueOf() === data.latestListedChangeAt.valueOf()); | ||
}).timeout(totalTimeout); | ||
|
||
afterEach(async () => { | ||
await clearAuth(); | ||
await clearFirestore(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters