What's the proper Loopback filter format to query a value in a MySQL (5.7) JSON field? For example, how would we perform this query using a Loopback REST filter?
QUERY
SELECT name, info->"$.id", info->"$.team"
FROM employee
WHERE JSON_EXTRACT(info, "$.team") = "A"
ORDER BY info->"$.id";
SAMPLE DATA
+---------------------------+----------
| info | name
+---------------------------+---------
| {"id": "1", "team": "A"} | "Sam"
| {"id": "2", "team": "A"} | "Julie"
| {"id": "3", "name": "B"} | "Rob"
| {"id": "4", "name": "B"} | "Cindy"
+---------------------------+---------
UNSUCCESSFUL ATTEMPTS
/employee?filter[where][info->$.team]=A
/employee?filter[where][info.team]=A
/employee?filter[where][info][team]=A
Since this question is the first to pop from Google search, and that the loopback mysql connector still does not allow for json querying, I feel like a proper answer should be given for future readers.
The work around is to add the feature directly on the connector by yourself, until the loopback politics settle on a decision on how to really handle it.
Here's our take: put this inside your /connectors
folder:
import * as connector from "loopback-connector-mysql"
var g = require('strong-globalize')();
var SqlConnector = require('loopback-connector').SqlConnector;
var ParameterizedSQL = SqlConnector.ParameterizedSQL;
const _buildExpression = connector.MySQL.prototype.buildExpression;
connector.MySQL.prototype.buildExpression = function (columnName, operator, operatorValue, propertyDefinition) {
if (operator === 'json') {
operatorValue = JSON.parse(operatorValue);
const keys = Object.keys(operatorValue);
if (keys.length > 1) {
g.warn('{{MySQL}} {{json}} syntax can only receive one key, received ' + keys.length);
}
const jsonPointer = "$." + keys[0];
const value = operatorValue[keys[0]];
const column = `JSON_EXTRACT(${columnName}, "${jsonPointer}")`;
if (value && value.constructor === Object) {
// this includes another operator, apply it on the built column
const operator = Object.keys(value)[0];
return _buildExpression.apply(this, [column, operator, value[operator], propertyDefinition]);
}
const clause = `${column} = ?`;
return new ParameterizedSQL(clause,
[value]);
} else {
return _buildExpression.apply(this, [columnName, operator, operatorValue, propertyDefinition])
}
};
export default connector;
Then point to this file in your database config connector: 'connectors/mysql-json'
or require it into the connector if that doesn't work (doc says we can define a path but we could not get it working...)
This is pretty simple, we overwrite the buildExpression
function to be able to insert a new operator json
. This will make it usable anywhere you'd use other operators such as gt
, lt
, nin
etc.
We went a step further and allow operators be passed in your json operator, to be able to leverage them too.
Here's an example query filter:
{"where":
{
"jsonProperty":{"json":{"id":1}}
}
}
// Results in
// WHERE JSON_EXTRACT('jsonProperty', '$.id') = 1
{"where":
{
"jsonProperty":{"json":{"id":{"gt":1}}}
}
}
// Results in
// WHERE JSON_EXTRACT(`jsonProperty`, '$.id') > 1
We simply prepend the key of the object passed to json
with $.
for ease of use (not sure if it's the best but works for us). You could write any json path as the key, just ommit the $.