-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
#4678: Converting strings to Date when schema.type is Date within agg… #4743
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
Changes from 10 commits
fa8c902
9a12b7c
044453a
90b43ba
1714711
867863e
d922a7a
7261ba0
beb54bb
403e81b
9526c65
7993f45
f8c06d9
abf42d9
f7edfa5
cdc90e2
52b2f7f
ae3ce3b
da3afbb
a7c428d
cc25079
1a51a82
af531c1
6b8fded
f8d7826
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -565,18 +565,10 @@ export class MongoStorageAdapter implements StorageAdapter { | |
} | ||
} | ||
if (stage.$match) { | ||
for (const field in stage.$match) { | ||
if (schema.fields[field] && schema.fields[field].type === 'Pointer') { | ||
const transformMatch = { [`_p_${field}`] : `${schema.fields[field].targetClass}$${stage.$match[field]}` }; | ||
stage.$match = transformMatch; | ||
} | ||
if (field === 'objectId') { | ||
const transformMatch = Object.assign({}, stage.$match); | ||
transformMatch._id = stage.$match[field]; | ||
delete transformMatch.objectId; | ||
stage.$match = transformMatch; | ||
} | ||
} | ||
stage.$match = this._parseAggregateArgs(schema, stage.$match); | ||
} | ||
if (stage.$project) { | ||
stage.$project = this._parseAggregateProjectArgs(schema, stage.$project); | ||
} | ||
return stage; | ||
}); | ||
|
@@ -608,6 +600,113 @@ export class MongoStorageAdapter implements StorageAdapter { | |
.catch(err => this.handleError(err)); | ||
} | ||
|
||
// This function will recursively traverse the pipeline and convert any Pointer or Date columns. | ||
// If we detect a pointer column we will rename the column being queried for to match the column | ||
// in the database. We also modify the value to what we expect the value to be in the database | ||
// as well. | ||
// For dates, the driver expects a Date object, but we have a string coming in. So we'll convert | ||
// the string to a Date so the driver can perform the necessary comparison. | ||
// | ||
// The goal of this method is to look for the "leaves" of the pipeline and determine if it needs | ||
// to be converted. The pipeline can have a few different forms. For more details, see: | ||
// https://docs.mongodb.com/manual/reference/operator/aggregation/ | ||
// | ||
// If the pipeline is an array, it means we are probably parsing an '$and' or '$or' operator. In | ||
// that case we need to loop through all of it's children to find the columns being operated on. | ||
// If the pipeline is an object, then we'll loop through the keys checking to see if the key name | ||
// matches one of the schema columns. If it does match a column and the column is a Pointer or | ||
// a Date, then we'll convert the value as described above. | ||
// | ||
// As much as I hate recursion...this seemed like a good fit for it. We're essentially traversing | ||
// down a tree to find a "leaf node" and checking to see if it needs to be converted. | ||
_parseAggregateArgs(schema: any, pipeline: any): any { | ||
if (Array.isArray(pipeline)) { | ||
return pipeline.map((value) => this._parseAggregateArgs(schema, value)); | ||
} | ||
else if (typeof pipeline === 'object') { | ||
const rtnval = {}; | ||
for (const field in pipeline) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. declare |
||
if (schema.fields[field] && schema.fields[field].type === 'Pointer') { | ||
if (typeof pipeline[field] === 'object') { | ||
// | ||
// Pass objects down to MongoDB...this is more than likely an $exists operator. | ||
// | ||
rtnval[`_p_${field}`] = pipeline[field]; | ||
} | ||
else { | ||
rtnval[`_p_${field}`] = `${schema.fields[field].targetClass}$${pipeline[field]}`; | ||
} | ||
} else if (schema.fields[field] && schema.fields[field].type === 'Date') { | ||
rtnval[field] = this._convertToDate(pipeline[field]); | ||
} else { | ||
rtnval[field] = this._parseAggregateArgs(schema, pipeline[field]); | ||
} | ||
|
||
if (field === 'objectId') { | ||
rtnval['_id'] = rtnval[field]; | ||
delete rtnval[field]; | ||
} else if (field === 'createdAt') { | ||
rtnval['_created_at'] = rtnval[field]; | ||
delete rtnval[field]; | ||
} else if (field === 'updatedAt') { | ||
rtnval['_updated_at'] = rtnval[field]; | ||
delete rtnval[field]; | ||
} | ||
} | ||
return rtnval; | ||
} | ||
return pipeline; | ||
} | ||
|
||
// This function is slightly different than the one above. Rather than trying to combine these | ||
// two functions and making the code even harder to understand, I decided to split it up. The | ||
// difference with this function is we are not transforming the values, only the keys of the | ||
// pipeline. | ||
_parseAggregateProjectArgs(schema: any, pipeline: any): any { | ||
if (Array.isArray(pipeline)) { | ||
return pipeline.map((value) => this._parseAggregateProjectArgs(schema, value)); | ||
} | ||
else if (typeof pipeline === 'object') { | ||
const rtnval = {}; | ||
for (const field in pipeline) { | ||
if (schema.fields[field] && schema.fields[field].type === 'Pointer') { | ||
rtnval[`_p_${field}`] = pipeline[field]; | ||
} else { | ||
rtnval[field] = this._parseAggregateArgs(schema, pipeline[field]); | ||
} | ||
|
||
if (field === 'objectId') { | ||
rtnval['_id'] = rtnval[field]; | ||
delete rtnval[field]; | ||
} else if (field === 'createdAt') { | ||
rtnval['_created_at'] = rtnval[field]; | ||
delete rtnval[field]; | ||
} else if (field === 'updatedAt') { | ||
rtnval['_updated_at'] = rtnval[field]; | ||
delete rtnval[field]; | ||
} | ||
} | ||
return rtnval; | ||
} | ||
return pipeline; | ||
} | ||
|
||
// This function will attempt to convert the provided value to a Date object. Since this is part | ||
// of an aggregation pipeline, the value can either be a string or it can be another object with | ||
// an operator in it (like $gt, $lt, etc). Because of this I felt it was easier to make this a | ||
// recursive method to traverse down to the "leaf node" which is going to be the string. | ||
_convertToDate(value: any): any { | ||
if (typeof value === 'string') { | ||
return new Date(value); | ||
} | ||
|
||
const rtnval = {} | ||
for (const field in value) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we document what we expect the value to look like, I have trouble understanding what it does, and why we need the iterator, and recursion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry for the newbie question...how do we take care of these? I've updated my branch to include your suggestions. Thanks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function may work wrong way when "value" is |
||
rtnval[field] = this._convertToDate(value[field]) | ||
} | ||
return rtnval; | ||
} | ||
|
||
_parseReadPreference(readPreference: ?string): ?string { | ||
switch (readPreference) { | ||
case 'PRIMARY': | ||
|
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 see in the travis logs that this catch gets called.
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.
@cjbland Can you remove the catch from this test? It does pass now