GraphQL: DX Relational Where Query (#6255)

* DX Relational Where Query

* Remove WherePointer & fix tests

* Add have, haveNot, exists on Pointer/Relation where input

* Merge branch 'master' into gql-relational-query

* Enable inQueryKey

* better descrption
This commit is contained in:
Antoine Cormouls
2019-12-05 19:14:16 +01:00
committed by Antonio Davi Macedo Coelho de Castro
parent afe49cb1f7
commit 5d76b2f354
7 changed files with 519 additions and 160 deletions

View File

@@ -4915,7 +4915,11 @@ describe('ParseGraphQLServer', () => {
OR: [
{
pointerToUser: {
equalTo: user5.id,
have: {
objectId: {
equalTo: user5.id,
},
},
},
},
{
@@ -4960,7 +4964,11 @@ describe('ParseGraphQLServer', () => {
variables: {
where: {
pointerToUser: {
in: [user5.id],
have: {
objectId: {
in: [user5.id],
},
},
},
},
},
@@ -5063,6 +5071,72 @@ describe('ParseGraphQLServer', () => {
}
});
it('should support in query key', async () => {
try {
const country = new Parse.Object('Country');
country.set('code', 'FR');
await country.save();
const country2 = new Parse.Object('Country');
country2.set('code', 'US');
await country2.save();
const city = new Parse.Object('City');
city.set('country', 'FR');
city.set('name', 'city1');
await city.save();
const city2 = new Parse.Object('City');
city2.set('country', 'US');
city2.set('name', 'city2');
await city2.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const {
data: {
cities: { edges: result },
},
} = await apolloClient.query({
query: gql`
query inQueryKey($where: CityWhereInput) {
cities(where: $where) {
edges {
node {
country
name
}
}
}
}
`,
context: {
headers: {
'X-Parse-Master-Key': 'test',
},
},
variables: {
where: {
country: {
inQueryKey: {
query: {
className: 'Country',
where: { code: { equalTo: 'US' } },
},
key: 'code',
},
},
},
},
});
expect(result.length).toEqual(1);
expect(result[0].node.name).toEqual('city2');
} catch (e) {
handleError(e);
}
});
it('should support order, skip and first arguments', async () => {
const promises = [];
for (let i = 0; i < 100; i++) {
@@ -5278,7 +5352,11 @@ describe('ParseGraphQLServer', () => {
OR: [
{
pointerToUser: {
equalTo: user5.id,
have: {
objectId: {
equalTo: user5.id,
},
},
},
},
{
@@ -5332,7 +5410,11 @@ describe('ParseGraphQLServer', () => {
OR: [
{
pointerToUser: {
equalTo: user5.id,
have: {
objectId: {
equalTo: user5.id,
},
},
},
},
{
@@ -5746,7 +5828,11 @@ describe('ParseGraphQLServer', () => {
variables: {
where: {
pointerToUser: {
inQuery: { where: {}, className: '_User' },
have: {
objectId: {
equalTo: 'xxxx',
},
},
},
},
},
@@ -8557,6 +8643,236 @@ describe('ParseGraphQLServer', () => {
expect(result2.companies.edges[0].node.objectId).toEqual(company1.id);
});
it_only_db('mongo')(
'should support relational where query',
async () => {
const president = new Parse.Object('President');
president.set('name', 'James');
await president.save();
const employee = new Parse.Object('Employee');
employee.set('name', 'John');
await employee.save();
const company1 = new Parse.Object('Company');
company1.set('name', 'imACompany1');
await company1.save();
const company2 = new Parse.Object('Company');
company2.set('name', 'imACompany2');
company2.relation('employees').add([employee]);
await company2.save();
const country = new Parse.Object('Country');
country.set('name', 'imACountry');
country.relation('companies').add([company1, company2]);
await country.save();
const country2 = new Parse.Object('Country');
country2.set('name', 'imACountry2');
country2.relation('companies').add([company1]);
await country2.save();
const country3 = new Parse.Object('Country');
country3.set('name', 'imACountry3');
country3.set('president', president);
await country3.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
let {
data: {
countries: { edges: result },
},
} = await apolloClient.query({
query: gql`
query findCountry($where: CountryWhereInput) {
countries(where: $where) {
edges {
node {
id
objectId
companies {
edges {
node {
id
objectId
name
}
}
}
}
}
}
}
`,
variables: {
where: {
companies: {
have: {
employees: { have: { name: { equalTo: 'John' } } },
},
},
},
},
});
expect(result.length).toEqual(1);
result = result[0].node;
expect(result.objectId).toEqual(country.id);
expect(result.companies.edges.length).toEqual(2);
const {
data: {
countries: { edges: result2 },
},
} = await apolloClient.query({
query: gql`
query findCountry($where: CountryWhereInput) {
countries(where: $where) {
edges {
node {
id
objectId
companies {
edges {
node {
id
objectId
name
}
}
}
}
}
}
}
`,
variables: {
where: {
companies: {
have: {
OR: [
{ name: { equalTo: 'imACompany1' } },
{ name: { equalTo: 'imACompany2' } },
],
},
},
},
},
});
expect(result2.length).toEqual(2);
const {
data: {
countries: { edges: result3 },
},
} = await apolloClient.query({
query: gql`
query findCountry($where: CountryWhereInput) {
countries(where: $where) {
edges {
node {
id
name
}
}
}
}
`,
variables: {
where: {
companies: { exists: false },
},
},
});
expect(result3.length).toEqual(1);
expect(result3[0].node.name).toEqual('imACountry3');
const {
data: {
countries: { edges: result4 },
},
} = await apolloClient.query({
query: gql`
query findCountry($where: CountryWhereInput) {
countries(where: $where) {
edges {
node {
id
name
}
}
}
}
`,
variables: {
where: {
president: { exists: false },
},
},
});
expect(result4.length).toEqual(2);
const {
data: {
countries: { edges: result5 },
},
} = await apolloClient.query({
query: gql`
query findCountry($where: CountryWhereInput) {
countries(where: $where) {
edges {
node {
id
name
}
}
}
}
`,
variables: {
where: {
president: { exists: true },
},
},
});
expect(result5.length).toEqual(1);
const {
data: {
countries: { edges: result6 },
},
} = await apolloClient.query({
query: gql`
query findCountry($where: CountryWhereInput) {
countries(where: $where) {
edges {
node {
id
objectId
name
}
}
}
}
`,
variables: {
where: {
companies: {
haveNot: {
OR: [
{ name: { equalTo: 'imACompany1' } },
{ name: { equalTo: 'imACompany2' } },
],
},
},
},
},
});
expect(result6.length).toEqual(1);
expect(result6.length).toEqual(1);
expect(result6[0].node.name).toEqual('imACountry3');
}
);
it('should support files', async () => {
try {
parseServer = await global.reconfigureServer({

View File

@@ -67,13 +67,12 @@ const findObjects = async (
auth,
info,
selectedFields,
fields
parseClasses
) => {
if (!where) {
where = {};
}
transformQueryInputToParse(where, fields, className);
transformQueryInputToParse(where, className, parseClasses);
const skipAndLimitCalculation = calculateSkipAndLimit(
skipInput,
first,

View File

@@ -713,35 +713,6 @@ const COUNT_ATT = {
type: new GraphQLNonNull(GraphQLInt),
};
const SUBQUERY_INPUT = new GraphQLInputObjectType({
name: 'SubqueryInput',
description:
'The SubqueryInput type is used to specify a sub query to another class.',
fields: {
className: CLASS_NAME_ATT,
where: Object.assign({}, WHERE_ATT, {
type: new GraphQLNonNull(WHERE_ATT.type),
}),
},
});
const SELECT_INPUT = new GraphQLInputObjectType({
name: 'SelectInput',
description:
'The SelectInput type is used to specify an inQueryKey or a notInQueryKey operation on a constraint.',
fields: {
query: {
description: 'This is the subquery to be executed.',
type: new GraphQLNonNull(SUBQUERY_INPUT),
},
key: {
description:
'This is the key in the result of the subquery that must match (not match) the field.',
type: new GraphQLNonNull(GraphQLString),
},
},
});
const SEARCH_INPUT = new GraphQLInputObjectType({
name: 'SearchInput',
description:
@@ -907,18 +878,6 @@ const exists = {
type: GraphQLBoolean,
};
const inQueryKey = {
description:
'This is the inQueryKey operator to specify a constraint to select the objects where a field equals to a key in the result of a different query.',
type: SELECT_INPUT,
};
const notInQueryKey = {
description:
'This is the notInQueryKey operator to specify a constraint to select the objects where a field do not equal to a key in the result of a different query.',
type: SELECT_INPUT,
};
const matchesRegex = {
description:
'This is the matchesRegex operator to specify a constraint to select the objects where the value of a field matches a specified regular expression.',
@@ -931,6 +890,47 @@ const options = {
type: GraphQLString,
};
const SUBQUERY_INPUT = new GraphQLInputObjectType({
name: 'SubqueryInput',
description:
'The SubqueryInput type is used to specify a sub query to another class.',
fields: {
className: CLASS_NAME_ATT,
where: Object.assign({}, WHERE_ATT, {
type: new GraphQLNonNull(WHERE_ATT.type),
}),
},
});
const SELECT_INPUT = new GraphQLInputObjectType({
name: 'SelectInput',
description:
'The SelectInput type is used to specify an inQueryKey or a notInQueryKey operation on a constraint.',
fields: {
query: {
description: 'This is the subquery to be executed.',
type: new GraphQLNonNull(SUBQUERY_INPUT),
},
key: {
description:
'This is the key in the result of the subquery that must match (not match) the field.',
type: new GraphQLNonNull(GraphQLString),
},
},
});
const inQueryKey = {
description:
'This is the inQueryKey operator to specify a constraint to select the objects where a field equals to a key in the result of a different query.',
type: SELECT_INPUT,
};
const notInQueryKey = {
description:
'This is the notInQueryKey operator to specify a constraint to select the objects where a field do not equal to a key in the result of a different query.',
type: SELECT_INPUT,
};
const ID_WHERE_INPUT = new GraphQLInputObjectType({
name: 'IdWhereInput',
description:
@@ -964,8 +964,6 @@ const STRING_WHERE_INPUT = new GraphQLInputObjectType({
in: inOp(GraphQLString),
notIn: notIn(GraphQLString),
exists,
inQueryKey,
notInQueryKey,
matchesRegex,
options,
text: {
@@ -973,6 +971,8 @@ const STRING_WHERE_INPUT = new GraphQLInputObjectType({
'This is the $text operator to specify a full text search constraint.',
type: TEXT_INPUT,
},
inQueryKey,
notInQueryKey,
},
});
@@ -1022,8 +1022,6 @@ const ARRAY_WHERE_INPUT = new GraphQLInputObjectType({
in: inOp(ANY),
notIn: notIn(ANY),
exists,
inQueryKey,
notInQueryKey,
containedBy: {
description:
'This is the containedBy operator to specify a constraint to select the objects where the values of an array field is contained by another specified array.',
@@ -1034,6 +1032,8 @@ const ARRAY_WHERE_INPUT = new GraphQLInputObjectType({
'This is the contains operator to specify a constraint to select the objects where the values of an array field contain all elements of another specified array.',
type: new GraphQLList(ANY),
},
inQueryKey,
notInQueryKey,
},
});
@@ -1123,10 +1123,10 @@ const FILE_WHERE_INPUT = new GraphQLInputObjectType({
in: inOp(FILE),
notIn: notIn(FILE),
exists,
inQueryKey,
notInQueryKey,
matchesRegex,
options,
inQueryKey,
notInQueryKey,
},
});
@@ -1249,8 +1249,6 @@ const load = parseGraphQLSchema => {
parseGraphQLSchema.addGraphQLType(PARSE_OBJECT, true);
parseGraphQLSchema.addGraphQLType(READ_PREFERENCE, true);
parseGraphQLSchema.addGraphQLType(READ_OPTIONS_INPUT, true);
parseGraphQLSchema.addGraphQLType(SUBQUERY_INPUT, true);
parseGraphQLSchema.addGraphQLType(SELECT_INPUT, true);
parseGraphQLSchema.addGraphQLType(SEARCH_INPUT, true);
parseGraphQLSchema.addGraphQLType(TEXT_INPUT, true);
parseGraphQLSchema.addGraphQLType(BOX_INPUT, true);
@@ -1279,6 +1277,8 @@ const load = parseGraphQLSchema => {
parseGraphQLSchema.addGraphQLType(USER_ACL, true);
parseGraphQLSchema.addGraphQLType(ROLE_ACL, true);
parseGraphQLSchema.addGraphQLType(PUBLIC_ACL, true);
parseGraphQLSchema.addGraphQLType(SUBQUERY_INPUT, true);
parseGraphQLSchema.addGraphQLType(SELECT_INPUT, true);
};
export {
@@ -1297,6 +1297,8 @@ export {
DATE,
BYTES,
parseFileValue,
SUBQUERY_INPUT,
SELECT_INPUT,
FILE,
FILE_INFO,
GEO_POINT_FIELDS,
@@ -1326,8 +1328,6 @@ export {
SKIP_ATT,
LIMIT_ATT,
COUNT_ATT,
SUBQUERY_INPUT,
SELECT_INPUT,
SEARCH_INPUT,
TEXT_INPUT,
BOX_INPUT,
@@ -1344,10 +1344,10 @@ export {
inOp,
notIn,
exists,
inQueryKey,
notInQueryKey,
matchesRegex,
options,
inQueryKey,
notInQueryKey,
ID_WHERE_INPUT,
STRING_WHERE_INPUT,
NUMBER_WHERE_INPUT,

View File

@@ -145,7 +145,7 @@ const load = function(
auth,
info,
selectedFields,
parseClass.fields
parseGraphQLSchema.parseClasses
);
} catch (e) {
parseGraphQLSchema.handleError(e);

View File

@@ -5,6 +5,7 @@ import {
GraphQLList,
GraphQLInputObjectType,
GraphQLNonNull,
GraphQLBoolean,
GraphQLEnumType,
} from 'graphql';
import {
@@ -262,34 +263,6 @@ const load = (
parseGraphQLSchema.addGraphQLType(classGraphQLRelationType) ||
defaultGraphQLTypes.OBJECT;
const classGraphQLConstraintTypeName = `${graphQLClassName}PointerWhereInput`;
let classGraphQLConstraintType = new GraphQLInputObjectType({
name: classGraphQLConstraintTypeName,
description: `The ${classGraphQLConstraintTypeName} input type is used in operations that involve filtering objects by a pointer field to ${graphQLClassName} class.`,
fields: {
equalTo: defaultGraphQLTypes.equalTo(GraphQLID),
notEqualTo: defaultGraphQLTypes.notEqualTo(GraphQLID),
in: defaultGraphQLTypes.inOp(defaultGraphQLTypes.OBJECT_ID),
notIn: defaultGraphQLTypes.notIn(defaultGraphQLTypes.OBJECT_ID),
exists: defaultGraphQLTypes.exists,
inQueryKey: defaultGraphQLTypes.inQueryKey,
notInQueryKey: defaultGraphQLTypes.notInQueryKey,
inQuery: {
description:
'This is the inQuery operator to specify a constraint to select the objects where a field equals to any of the object ids in the result of a different query.',
type: defaultGraphQLTypes.SUBQUERY_INPUT,
},
notInQuery: {
description:
'This is the notInQuery operator to specify a constraint to select the objects where a field do not equal to any of the object ids in the result of a different query.',
type: defaultGraphQLTypes.SUBQUERY_INPUT,
},
},
});
classGraphQLConstraintType = parseGraphQLSchema.addGraphQLType(
classGraphQLConstraintType
);
const classGraphQLConstraintsTypeName = `${graphQLClassName}WhereInput`;
let classGraphQLConstraintsType = new GraphQLInputObjectType({
name: classGraphQLConstraintsTypeName,
@@ -339,6 +312,31 @@ const load = (
parseGraphQLSchema.addGraphQLType(classGraphQLConstraintsType) ||
defaultGraphQLTypes.OBJECT;
const classGraphQLRelationConstraintsTypeName = `${graphQLClassName}RelationWhereInput`;
let classGraphQLRelationConstraintsType = new GraphQLInputObjectType({
name: classGraphQLRelationConstraintsTypeName,
description: `The ${classGraphQLRelationConstraintsTypeName} input type is used in operations that involve filtering objects of ${graphQLClassName} class.`,
fields: () => ({
have: {
description:
'Run a relational/pointer query where at least one child object can match.',
type: classGraphQLConstraintsType,
},
haveNot: {
description:
'Run an inverted relational/pointer query where at least one child object can match.',
type: classGraphQLConstraintsType,
},
exists: {
description: 'Check if the relation/pointer contains objects.',
type: GraphQLBoolean,
},
}),
});
classGraphQLRelationConstraintsType =
parseGraphQLSchema.addGraphQLType(classGraphQLRelationConstraintsType) ||
defaultGraphQLTypes.OBJECT;
const classGraphQLOrderTypeName = `${graphQLClassName}Order`;
let classGraphQLOrderType = new GraphQLEnumType({
name: classGraphQLOrderTypeName,
@@ -464,10 +462,7 @@ const load = (
auth,
info,
selectedFields,
parseGraphQLSchema.parseClasses.find(
parseClass =>
parseClass.className === source[field].className
).fields
parseGraphQLSchema.parseClasses
);
} catch (e) {
parseGraphQLSchema.handleError(e);
@@ -558,8 +553,8 @@ const load = (
classGraphQLRelationType,
classGraphQLCreateType,
classGraphQLUpdateType,
classGraphQLConstraintType,
classGraphQLConstraintsType,
classGraphQLRelationConstraintsType,
classGraphQLFindArgs,
classGraphQLOutputType,
classGraphQLFindResultType,

View File

@@ -26,9 +26,9 @@ const transformConstraintTypeToGraphQL = (
case 'Pointer':
if (
parseClassTypes[targetClass] &&
parseClassTypes[targetClass].classGraphQLConstraintType
parseClassTypes[targetClass].classGraphQLRelationConstraintsType
) {
return parseClassTypes[targetClass].classGraphQLConstraintType;
return parseClassTypes[targetClass].classGraphQLRelationConstraintsType;
} else {
return defaultGraphQLTypes.OBJECT;
}
@@ -43,6 +43,14 @@ const transformConstraintTypeToGraphQL = (
case 'ACL':
return defaultGraphQLTypes.OBJECT_WHERE_INPUT;
case 'Relation':
if (
parseClassTypes[targetClass] &&
parseClassTypes[targetClass].classGraphQLRelationConstraintsType
) {
return parseClassTypes[targetClass].classGraphQLRelationConstraintsType;
} else {
return defaultGraphQLTypes.OBJECT;
}
default:
return undefined;
}

View File

@@ -1,7 +1,6 @@
import { fromGlobalId } from 'graphql-relay';
const parseQueryMap = {
id: 'objectId',
OR: '$or',
AND: '$and',
NOR: '$nor',
@@ -47,13 +46,44 @@ const parseConstraintMap = {
const transformQueryConstraintInputToParse = (
constraints,
fields,
parentFieldName,
parentConstraints
className,
parentConstraints,
parseClasses
) => {
const fields = parseClasses.find(
parseClass => parseClass.className === className
).fields;
if (parentFieldName === 'id' && className) {
Object.keys(constraints).forEach(constraintName => {
const constraintValue = constraints[constraintName];
if (typeof constraintValue === 'string') {
const globalIdObject = fromGlobalId(constraintValue);
if (globalIdObject.type === className) {
constraints[constraintName] = globalIdObject.id;
}
} else if (Array.isArray(constraintValue)) {
constraints[constraintName] = constraintValue.map(value => {
const globalIdObject = fromGlobalId(value);
if (globalIdObject.type === className) {
return globalIdObject.id;
}
return value;
});
}
});
parentConstraints.objectId = constraints;
delete parentConstraints.id;
}
Object.keys(constraints).forEach(fieldName => {
let fieldValue = constraints[fieldName];
if (parseConstraintMap[fieldName]) {
constraints[parseConstraintMap[fieldName]] = constraints[fieldName];
delete constraints[fieldName];
}
/**
* If we have a key-value pair, we need to change the way the constraint is structured.
*
@@ -91,30 +121,65 @@ const transformQueryConstraintInputToParse = (
...parentConstraints[`${parentFieldName}.${fieldValue.key}`],
[parseConstraintMap[fieldName]]: fieldValue.value,
};
} else if (parseConstraintMap[fieldName]) {
delete constraints[fieldName];
fieldName = parseConstraintMap[fieldName];
constraints[fieldName] = fieldValue;
// If parent field type is Pointer, changes constraint value to format expected
// by Parse.
if (
fields[parentFieldName] &&
fields[parentFieldName].type === 'Pointer' &&
typeof fieldValue === 'string'
) {
const { targetClass } = fields[parentFieldName];
let objectId = fieldValue;
const globalIdObject = fromGlobalId(objectId);
if (globalIdObject.type === targetClass) {
objectId = globalIdObject.id;
} else if (
fields[parentFieldName] &&
(fields[parentFieldName].type === 'Pointer' ||
fields[parentFieldName].type === 'Relation')
) {
const { targetClass } = fields[parentFieldName];
if (fieldName === 'exists') {
if (fields[parentFieldName].type === 'Relation') {
const whereTarget = fieldValue ? 'where' : 'notWhere';
if (constraints[whereTarget]) {
if (constraints[whereTarget].objectId) {
constraints[whereTarget].objectId = {
...constraints[whereTarget].objectId,
$exists: fieldValue,
};
} else {
constraints[whereTarget].objectId = {
$exists: fieldValue,
};
}
} else {
const parseWhereTarget = fieldValue ? '$inQuery' : '$notInQuery';
parentConstraints[parentFieldName][parseWhereTarget] = {
where: { objectId: { $exists: true } },
className: targetClass,
};
}
delete constraints.$exists;
} else {
parentConstraints[parentFieldName].$exists = fieldValue;
}
constraints[fieldName] = {
__type: 'Pointer',
className: targetClass,
objectId,
};
return;
}
switch (fieldName) {
case 'have':
parentConstraints[parentFieldName].$inQuery = {
where: fieldValue,
className: targetClass,
};
transformQueryInputToParse(
parentConstraints[parentFieldName].$inQuery.where,
targetClass,
parseClasses
);
break;
case 'haveNot':
parentConstraints[parentFieldName].$notInQuery = {
where: fieldValue,
className: targetClass,
};
transformQueryInputToParse(
parentConstraints[parentFieldName].$notInQuery.where,
targetClass,
parseClasses
);
break;
}
delete constraints[fieldName];
return;
}
switch (fieldName) {
case '$point':
@@ -170,20 +235,21 @@ const transformQueryConstraintInputToParse = (
}
if (typeof fieldValue === 'object') {
if (fieldName === 'where') {
transformQueryInputToParse(fieldValue);
transformQueryInputToParse(fieldValue, className, parseClasses);
} else {
transformQueryConstraintInputToParse(
fieldValue,
fields,
fieldName,
constraints
className,
constraints,
parseClasses
);
}
}
});
};
const transformQueryInputToParse = (constraints, fields, className) => {
const transformQueryInputToParse = (constraints, className, parseClasses) => {
if (!constraints || typeof constraints !== 'object') {
return;
}
@@ -195,42 +261,17 @@ const transformQueryInputToParse = (constraints, fields, className) => {
delete constraints[fieldName];
fieldName = parseQueryMap[fieldName];
constraints[fieldName] = fieldValue;
if (fieldName !== 'objectId') {
fieldValue.forEach(fieldValueItem => {
transformQueryInputToParse(fieldValueItem, fields, className);
});
return;
} else if (className) {
Object.keys(fieldValue).forEach(constraintName => {
const constraintValue = fieldValue[constraintName];
if (typeof constraintValue === 'string') {
const globalIdObject = fromGlobalId(constraintValue);
if (globalIdObject.type === className) {
fieldValue[constraintName] = globalIdObject.id;
}
} else if (Array.isArray(constraintValue)) {
fieldValue[constraintName] = constraintValue.map(value => {
const globalIdObject = fromGlobalId(value);
if (globalIdObject.type === className) {
return globalIdObject.id;
}
return value;
});
}
});
}
}
if (typeof fieldValue === 'object') {
fieldValue.forEach(fieldValueItem => {
transformQueryInputToParse(fieldValueItem, className, parseClasses);
});
return;
} else {
transformQueryConstraintInputToParse(
fieldValue,
fields,
fieldName,
constraints
className,
constraints,
parseClasses
);
}
});