-
Notifications
You must be signed in to change notification settings - Fork 507
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Refreshcasestats using Functions/Firestore #1988
base: master
Are you sure you want to change the base?
WIP: Refreshcasestats using Functions/Firestore #1988
Conversation
"test": "firebase --project=$GCLOUD_PROJECT emulators:exec \"mocha -r ts-node/register src/**/*.spec.ts\"", | ||
"test_rules": "firebase --project=mblain-13nov2020 emulators:exec \"mocha -r ts-node/register src/**/*.spec.ts\"", | ||
"test": "mocha -r ts-node/register --reporter spec", | ||
"check-location": "./check_location.sh", | ||
"serve": "npm run build && firebase emulators:start", | ||
"shell": "npm run build && firebase functions:shell", | ||
"preserve": "tsc", | ||
"serve": "firebase emulators:start --project=mblain-13nov2020", | ||
"shell": "npm run build np&& firebase functions:shell", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was there a usability issue with the tests that you're working around here? I'm concerned that in this config just running npm test
will not work unless you knew to previously start the emulators, but if the tests were hard for you to use prior to this change we should address that!
Also, we'll want to get rid of hardcoded project ID's before submitting this, obviously. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current system assumes my shell is bash and some other things. My version is worse in that it hardcodes some stuff. I think there's some package.json way to do the dependecies (build -> serve). Interestingly now if I do npm run serve it does get it right. Test, not sure.
isoCountryCode: string; | ||
subscribedTopics: string[]; | ||
} | ||
|
||
refreshcasestats.setDb(db); /// This seems wrong??????????????????????????????????????????????????????????? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's fine to run...
admin.initializeApp();
const db = admin.firestore();
... in every file where we need database access. Alternatively, we can create a db.ts
which has those two lines and an export db
that we can then import in all the other files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not seem to be correct. If I call it in both files, I get an error like this:
! Error: The default Firebase app already exists. This means you called initializeApp() more than once without providing an app name as the second argument. In most cases you only need to call initializeApp() once. But if you do want to initialize multiple apps, pass a second argument to initializeApp() to give each app a unique name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh shoot, that unfortunately makes a lot of sense.
In that case, I'd go for a separate db.ts
module that we import from everywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a new module seems to work. Not sure if I did it correctly.
import { SERVING_REGION } from "./config"; | ||
import * as refreshcasestats from "./refreshcasestats"; // Is this right? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine! You might also consider importing only specific symbols (like we do from ./config
above), notably refreshCaseStats
, so that the export (further down) can just be export refreshCaseStats;
- but only once we can remove the setDb()
call, since that would look very strange without the context of refreshcasestats
.
server/functions/src/index.ts
Outdated
jurisdiction.code | ||
}`; | ||
console.log(`Loading data for ${key}`); | ||
let loadedData = await db.collection("StoredCaseStats").doc(key).get(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll probably want to make "StoredCaseStats"
a constant loaded from refreshcasestats.ts
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the database schema question! What are the entity kind names, and where is that documented in the code? Is it shared 'client' and 'server'? For now, I added a const.
server/functions/src/index.ts
Outdated
const data = JSON.parse(fs.readFileSync("casestats.json", "utf8")); | ||
response.status(200).json(data); | ||
getCaseStatsAsync(request.body) | ||
.then(function (resp) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's generally preferred to use the now-more-common shorthand for an inline function definition:
(params) => { body }
There's a few examples throughout this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
// Description of the JSON data returned by the ArcGIS Server | ||
interface Attributes { | ||
ISO_2_CODE: string; | ||
date_epicrv: number; | ||
NewCase: number; | ||
CumCase: number; | ||
NewDeath: number; | ||
CumDeath: number; | ||
ADM0_NAME: string; | ||
} | ||
|
||
interface Feature { | ||
attributes: Attributes; | ||
} | ||
|
||
// The ARCGISResponse contains more than this, but all we need are the features. | ||
interface ArcGISResponse { | ||
features: Feature[]; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that we have this here, very nice code-as-documentation.
.https.onRequest((request, response) => { | ||
refreshCaseStatsAsync() | ||
.then(function (ok) { | ||
if (!ok) { | ||
response.status(500).send("Error"); | ||
return; | ||
} | ||
response.status(200).send("OK"); | ||
}) | ||
.catch(function (error) { | ||
console.log(error); | ||
response.status(500).send("Error"); // How to get the system to backoff/retry? What text to use? | ||
return; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use async/await here too (async (request, response) => { ... }
) if you'd like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
// TODO: Scheduled functions require billing (lol). It's only $0.10/month but | ||
// but that's absurd for a tiny project... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not even the functions that require billing... It's the scheduler that fires off the Pub/Sub message that triggers the function that needs it... Le sigh. 😕
FWIW, even on billing-enabled projects the free tier is in effect, so we'll likely still be charged $0.
// This was generated from who.proto with https://github.com/stephenh/ts-proto | ||
// Then all of the encoding methods were removed as they interfered with | ||
// Firestore and are not necessary. | ||
// This leaves only the interface definitions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this a lot! I in particular like how the use of these interfaces produces very nice self-documenting code in the other .ts
files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However it needs better tooling to keep in sync w/proto changes. Probably just some flags to ts-proto but I have not investigated.
server/functions/src/index.ts
Outdated
let resp = { | ||
globalStats: undefined, | ||
jurisdictionStats: [], | ||
ttl: 0, | ||
} as who.GetCaseStatsResponse; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider this slight change:
let resp: who.GetCaseStatsResponse = {
globalStats: undefined,
jurisdictionStats: [],
ttl: 0,
}
... Since the as
type assertion will not throw a type error if you're missing required fields (I'm basing that on e.g. this), but when constructing an object of a given type you will be warned about that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks! i didn't know how to do 'constructors' but copied another example in this file.
This item has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
See comments. |
If the emulators work the deploy will most likely work too, but we'll of course have to verify before we submit.
Interesting... Do you get the error 400 from The behavior you observe would be consistent with my expectations if the code failed to send an HTTP response before returning - but if there is a response being sent (as I believe the
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No major code issues to my eyes!
To me, the remaining work looks like:
- Factor out the
firebase.initializeApp(); export db = firebase.firestore();
into adb.ts
that can get imported by the other files. - Fix up the
package.json
so that it's easily usable (ideally no hardcoded project IDs, if that's not feasible then make the hardcoded values something likeYOUR-PROJECT-ID-HERE
and updateREADME.md
with instructions). - Unit tests where useful.
- Remove the HTTP function and replace with the scheduled async function (note that scheduled functions can be triggered manually too by clicking on the Cloud Console's Cloud Scheduler page).
- Deploy to a real project and verify there.
How does that sound?
if (lastSnapshot.dailyCases == 0 && lastSnapshot.dailyDeaths == 0) { | ||
if (priorSnapshot.dailyCases > 0 || priorSnapshot.dailyDeaths > 0) { | ||
// Likely "No Data Reported" | ||
timeseries.pop(); | ||
countryStat.lastUpdated = priorSnapshot.epochMsec; | ||
countriesLastDayRemoved.push(countryName); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that these if
s don't have else
s, consider using if (opposite) continue
here too, to improve readability by reducing indentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think having the exceptional case in an 'if' is more understandable.
This item has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
This item has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
This rewrites the refreshCaseStats functionality in Typescript on the Google Cloud Functions platform.
Current status is that it is functional: It can load the casestats from the WHO/ArcGIS endpoint and store them in the Datastore/Firestore. This functionality should be exactly the same as the old system. It has a getCaseStats endpoint which should return the data in the equivalent format as the old system.
What is missing?
#1972
Screenshots
N/A
How did you test the change?
curl
to a dev App Engine serverWell, curl to the google cloud functions/firestore emulator.
Checklist: