Removing @google-cloud/projectify#564
Conversation
Codecov Report
@@ Coverage Diff @@
## master #564 +/- ##
==========================================
+ Coverage 95.85% 95.88% +0.02%
==========================================
Files 24 24
Lines 1931 1969 +38
Branches 168 168
==========================================
+ Hits 1851 1888 +37
- Misses 57 58 +1
Partials 23 23
Continue to review full report at Codecov.
|
Codecov Report
@@ Coverage Diff @@
## master #564 +/- ##
==========================================
- Coverage 96.01% 95.63% -0.38%
==========================================
Files 24 24
Lines 1955 1969 +14
Branches 168 167 -1
==========================================
+ Hits 1877 1883 +6
- Misses 57 65 +8
Partials 21 21
Continue to review full report at Codecov.
|
fc040cf to
981d992
Compare
981d992 to
e55174c
Compare
mikelehen
left a comment
There was a problem hiding this comment.
Nice! Mostly LGTM other than a few nits and a gripe about RelativePath naming / inheritance.
| * @private | ||
| */ | ||
| get projectId(): string { | ||
| this.ensureClientInitialized(); |
There was a problem hiding this comment.
FWIW, this naming tripped me up. I was confused about ensureClientInitialized() vs initializeIfNeeded().
Looking at them, I would be tempted to rename ensureClientInitialized() to assertClientInitialized().
But looking closer, ensureClientInitialized() is only called in one other place (directly below) and it's actually redundant. So why not drop the method and just put the projectId_ !== undefined check here?
There was a problem hiding this comment.
I wanted to point out that this is an important step, but I also did not mean to confuse anyone. To err on the side of caution, I inlined it here and removed the redundant call in formattedName.
| } | ||
|
|
||
| return this._runTransaction(updateFunction, transactionOptions); | ||
| return this.initializeIfNeeded().then( |
There was a problem hiding this comment.
Are you intentionally not using async/await (here and in most but not all of the other calls to initializeIfNeeded())? I think it's generally cleaner.
There was a problem hiding this comment.
Yes, this is intentional. Async functions do not throw exceptions, instead they return a failed Promise. Since we want to validate our input synchronously, I did not use async/await in functions that perform validation.
| private ensureClientInitialized(): void { | ||
| if (this._projectId === undefined) { | ||
| // Note that we do not mention `initializeIfNeeded()` here to not leak the | ||
| // private API. |
There was a problem hiding this comment.
I suspect this method is going away, but if not I'd drop this comment since 1) it's not really important for somebody to know why you didn't mention it, and 2) in fact I don't think it's important to not mention it. Internal Errors are necessarily going to mention internal implementation details sometimes. I wouldn't have any concerns about "leaking" stuff... we want to make the error as useful as possible for us to debug what went wrong.
There was a problem hiding this comment.
I removed it. I do however thing that we should not mention initializeIfNeeded(). If we point out that we should have called initializeIfNeeded(), our users will do so instead of filing a GitHub issue.
| * The default database ID for this Firestore client. We do not yet expose the | ||
| * ability to use different databases. | ||
| */ | ||
| export const DATABASE_ID = '(default)'; |
There was a problem hiding this comment.
FWIW I had trouble guessing what this was when I saw it imported elsewhere. DEFAULT_DATABASE_ID would be much clearer.
There was a problem hiding this comment.
Sure thing. DEFAULT_DATABASE_ID it is.
| * @class | ||
| */ | ||
| export class ResourcePath extends Path<ResourcePath> { | ||
| export class RelativePath extends Path<RelativePath> { |
There was a problem hiding this comment.
So I'm having some trouble with this RelativePath class:
- It's not an arbitrary relative path (else isDocument() and isCollection() wouldn't be answerable).
- "Relative" probably isn't the defining characteristic of this class. In fact, the
Pathbase class is already a relative path. It seems like the unique methods (id(),isCollection(),isDocument()) are really characteristics of being a "resource path" rather than a "relative path." - It seems strange for ResourcePath to extend RelativePath. "A resource path is a relative path" doesn't hold. And it's confusing for a method that takes
path: RelativePaththatpathmay actually be an (absolute)ResourcePath.
I haven't analyzed the code to figure out how these classes are used and what other options might work. Naive idea fragments:
- I think the mobile SDKs deal with this by having all ResourcePaths be relative and we only create an "absolute" one at the point we serialize. But that has downsides (most notably we can't handle resource-paths in documents that point to to other projects).
- If there's a way to solve this via composition rather than inheritance (e.g. have a
DatabasePaththat contains projectId, databaseId, and resourcePath members), I'd be a fan. I haven't read it, but presumably this wikipedia article makes the argument for me about why composition should be preferred over inheritance: https://en.wikipedia.org/wiki/Composition_over_inheritance - Failing all that, if you renamed ResourcePath to AbsoluteResourcePath and RelativePath to ResourcePath that would actually solve most of the problems I listed above I think.
Perhaps any of these solutions will involve a lot of churn though, so we may want to punt to a separate PR.
There was a problem hiding this comment.
I kept the class structure as is. I actually think that inheritance makes sense here - we are special casing the RelativePath class and modify the existing behavior only slightly. Furthermore, we can use both classes interchangeably, without having to to rely on TypeScrips's intersection types (or introduce another interface).
With all that being said, I renamed the classes to ResourcePath and QualifiedResourcePath. This should address most of your concerns. While there is some churn, it's 70ish lines and I pushed in a single commit together with the rest of the feedback (5f93109).
| private readonly _firestore: Firestore; | ||
| private readonly _serializer: Serializer; | ||
| private readonly _writes: WriteOp[] = []; | ||
| private readonly _ops: Array<() => WriteOp> = []; |
There was a problem hiding this comment.
This probably deserves a comment... Part of me wants to rename it lazyOps too, though not sure that is necessary /better.
There was a problem hiding this comment.
Comment added.
0eba165 to
e20227f
Compare
e20227f to
89b5dbb
Compare
|
Feedback addressed. I also had to add back the |
7cf08ec to
b49fe31
Compare
08fedb9 to
a840e59
Compare
mikelehen
left a comment
There was a problem hiding this comment.
Basically LGTM. If you decide to rework ResourcePath / QualifiedResourcePath inheritance, let me know and I can take another look.
|
|
||
| /** | ||
| * Convenience method to match the ResourcePath API. This method always | ||
| * returns the current instance. The arguments is ignored. |
There was a problem hiding this comment.
Not a big fan of ignoring arguments. Somebody could theoretically call this incorrectly and never know. Seems like we should either throw unconditionally (if we expect this to never get called), respect the projectId, or assert the projectId matches the actual projectId.
There was a problem hiding this comment.
I renamed the argument to projectIdIfMissing here and in ResourcePath. When I started writing this, I originally added an assert - but that doesn't work since the projectId in the argument is always the Project ID of the SDK's Firestore project. The Project ID in the path itself may be an ID from a different project if we deserialize a DocumentReference from a document.
An alternative would be to remove this method and only call toQualifiedResourcePath if we need to. That makes the callsites more complicated, and I kinda like the simplicity of the current approach. So for now I have just updated the argument name and comment, but please do let me know if you'd rather want me to drop this method altogether.
There was a problem hiding this comment.
Oh, wow. So DocumentReference may wrap either a ResourcePath or a QualifiedResourcePath and so we have to make sure we preserve the projectId if it's already a QualifiedResourcePath, but insert the current projectId if it's not? I didn't realize that issue (partially because we don't deal with it in the mobile SDK). That is a bit funky, but I guess I don't feel strongly enough to suggest you rework this anymore...
There was a problem hiding this comment.
Another way to solve this would be to add Firestore as a member to ResourcePath. formattedName() could then figure out what it needs on its own. I'll leave this as an exercise for the reader for now.
| append(relativePath: RelativePath|string): ResourcePath { | ||
| return super.append(relativePath) as ResourcePath; | ||
| append(relativePath: ResourcePath|string): QualifiedResourcePath { | ||
| return super.append(relativePath) as QualifiedResourcePath; |
There was a problem hiding this comment.
I guess we broke the Path<T> trick that lets us inherit these methods as-is? And this cast here is safe because super.append() ends up calling our construct() which actually does return a QualifiedResourcePath, even though we end up losing that type information? This feels a bit hacky...
I guess to make this work using the Path<T> pattern (and be able to inherit methods w/o overriding and unsafe casting, etc.), we'd need to create an abstract ResourcePathBase<T> class with the shared functionality and then have ResourcePath and QualifiedResourcePath both inherit from it with the appropriate construct() method implemented. WDYT? Worth it?
There was a problem hiding this comment.
I debated doing this but I think this overcomplicates this class structure quite a bit. I added a comment to explain why this cast is safe.
| } | ||
|
|
||
| /** | ||
| * Constructs a new instance of RelativePath. |
There was a problem hiding this comment.
RelativePath => ResourcePath here and below.
|
|
||
| /** | ||
| * Convenience method to match the ResourcePath API. This method always | ||
| * returns the current instance. The arguments is ignored. |
There was a problem hiding this comment.
Oh, wow. So DocumentReference may wrap either a ResourcePath or a QualifiedResourcePath and so we have to make sure we preserve the projectId if it's already a QualifiedResourcePath, but insert the current projectId if it's not? I didn't realize that issue (partially because we don't deal with it in the mobile SDK). That is a bit funky, but I guess I don't feel strongly enough to suggest you rework this anymore...
|
LGTM other than CI failing. 😛 |
This PR removes the project ID replacement that relies on
@google-cloud/projectifyto replace the string{{projectId}}in all outgoing Proto messages. This is an improved version of the original PR that uses less async/await after some much appreciated pushback from @mikelehen (yay!).I kept some of the non-essential formatting changes from the first version of this PR in the first commit. There are no functional changes here.
The second commit (e55174c) still has a bunch of changes:
initializedIsNeeded()is called. This means that there is now an operation queue in WriteBatch, and that Query cursors are no longer serialized to Proto at the point of query construction.RelativePathas a new top-level (internal) class. This class is used in DocumentReferences and CollectionReferences when we do not yet know the project ID. RelativePaths get converted to ResourcePaths during serialization.