Skip to content
This repository was archived by the owner on Sep 3, 2021. It is now read-only.

Commit edc657f

Browse files
Full-text @search directive, regexp filter, & nested @cypher mutation updates (#543)
* updates test script for searchSchema tests * adds @search directive during schema augmentation * generates search argument and corresponding input object * wires together function arguments used for generating query search argument * wires together function arguments for adding query search argument * adds searchSchema export * adds support for full-text search indexes using @search directives * Update selections.js * removes unused imports importing "query" from express was unfortunately generated via auto-completion during development * separates node and relationship mutation translation functions includes translation functions for nested @cypher mutations * remaining separated translations functions eveything in translate.js not added into /translate/mutation.js is within this translate/translate.js, likely to be later moved into /translate/query.js * Update testSchema.js * Update customSchemaTest.js * Update testSchema.js * adds @search directives for corresponding tests * Update augmentSchemaTest.test.js * Update cypherTest.test.js * adds tests for @search directive query argument translation only currently available for a root-level "search" argument, errors if @search is used on non-String type field, or list-type String field (only non-list String types fields currently work), errors if more than one search index argument is used. The threshold argument is optional and applies to whichever single search index argument is provided, if one is provided (errors otherwise) * Update augmentSchemaTest.test.js * adds custom nested @cypher mutation tests for root-level UNWIND @cypher mutations * adds tests for errors thrown by invalid @search directive cases * removes moved file * adds two error tests for query field search argument * Update searchSchema.test.js * adds regexp filter to augmentation even for list type String fields, the filter is not a list, because only a single regexp argument value should be provided to compare against all values in the corresponding list property. Normally, list filters are also lists themselves, so that the user can provide multiple argument values, logically evaluating to an at-least-one match to the corresponding list property values * adds support for variable import / export WITH clauses in nested @cypher mutations * adds binary predicate for regexp filter to translation logic * Update testSchema.js * Update customSchemaTest.js * Update testSchema.js * Update augmentSchemaTest.test.js * Update cypherTest.test.js * Update cypherTest.test.js * Update augmentSchemaTest.test.js * adds tests for import / export WITH clauses used with nested @cypher mutations * adds 1 test and emoves unused args * adds fix for augmentation error from @relation with 0 property type fields for the issue shared in slack~
1 parent 06f1c87 commit edc657f

21 files changed

+6120
-2305
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"build-with-sourcemaps": "babel src --presets @babel/preset-env --out-dir dist --source-maps",
1717
"precommit": "lint-staged",
1818
"prepare": "npm run build",
19-
"test": "nyc --reporter=lcov ava test/unit/augmentSchemaTest.test.js test/unit/configTest.test.js test/unit/assertSchema.test.js test/unit/cypherTest.test.js test/unit/filterTest.test.js test/unit/filterTests.test.js test/unit/custom/cypherTest.test.js test/unit/experimental/augmentSchemaTest.test.js test/unit/experimental/cypherTest.test.js test/unit/experimental/custom/cypherTest.test.js",
19+
"test": "nyc --reporter=lcov ava test/unit/augmentSchemaTest.test.js test/unit/configTest.test.js test/unit/assertSchema.test.js test/unit/searchSchema.test.js test/unit/cypherTest.test.js test/unit/filterTest.test.js test/unit/filterTests.test.js test/unit/custom/cypherTest.test.js test/unit/experimental/augmentSchemaTest.test.js test/unit/experimental/cypherTest.test.js test/unit/experimental/custom/cypherTest.test.js",
2020
"parse-tck": "babel-node test/helpers/tck/parseTck.js",
2121
"test-tck": "nyc ava --fail-fast test/unit/filterTests.test.js",
2222
"report-coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",

src/augment/directives.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Kind, DirectiveLocation, GraphQLString } from 'graphql';
1+
import { Kind, DirectiveLocation, GraphQLString, GraphQLFloat } from 'graphql';
22
import { TypeWrappers, getFieldType } from './fields';
33
import {
44
buildDirectiveDefinition,
@@ -27,7 +27,8 @@ export const DirectiveDefinition = {
2727
ADDITIONAL_LABELS: 'additionalLabels',
2828
ID: 'id',
2929
UNIQUE: 'unique',
30-
INDEX: 'index'
30+
INDEX: 'index',
31+
SEARCH: 'search'
3132
};
3233

3334
// The name of Role type used in authorization logic
@@ -419,6 +420,20 @@ const directiveDefinitionBuilderMap = {
419420
name: DirectiveDefinition.INDEX,
420421
locations: [DirectiveLocation.FIELD_DEFINITION]
421422
};
423+
},
424+
[DirectiveDefinition.SEARCH]: ({ config }) => {
425+
return {
426+
name: DirectiveDefinition.SEARCH,
427+
args: [
428+
{
429+
name: 'index',
430+
type: {
431+
name: GraphQLString
432+
}
433+
}
434+
],
435+
locations: [DirectiveLocation.FIELD_DEFINITION]
436+
};
422437
}
423438
};
424439

src/augment/input-values.js

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { Kind, GraphQLInt, isInputObjectType } from 'graphql';
1+
import {
2+
Kind,
3+
GraphQLInt,
4+
isInputObjectType,
5+
GraphQLString,
6+
GraphQLFloat
7+
} from 'graphql';
28
import {
39
buildName,
410
buildNamedType,
@@ -57,6 +63,10 @@ export const FilteringArgument = {
5763
FILTER: 'filter'
5864
};
5965

66+
export const SearchArgument = {
67+
SEARCH: 'search'
68+
};
69+
6070
export const isDataSelectionArgument = name =>
6171
Object.values({
6272
...PagingArgument,
@@ -122,6 +132,7 @@ export const buildQueryFieldArguments = ({
122132
outputType,
123133
isUnionType,
124134
isListType,
135+
searchesType,
125136
typeDefinitionMap
126137
}) => {
127138
const isListField = isListTypeField({ field });
@@ -211,6 +222,31 @@ export const buildQueryFieldArguments = ({
211222
}
212223
}
213224
}
225+
if (name === SearchArgument.SEARCH && !isUnionType) {
226+
if (!isCypherField({ directives: fieldDirectives })) {
227+
if (searchesType) {
228+
const argumentIndex = fieldArguments.findIndex(
229+
arg => arg.name.value === SearchArgument.SEARCH
230+
);
231+
// Does overwrite
232+
if (argumentIndex === -1) {
233+
fieldArguments.push(
234+
buildQuerySearchArgument({
235+
typeName: outputType
236+
})
237+
);
238+
} else {
239+
fieldArguments.splice(
240+
argumentIndex,
241+
1,
242+
buildQuerySearchArgument({
243+
typeName: outputType
244+
})
245+
);
246+
}
247+
}
248+
}
249+
}
214250
});
215251
return fieldArguments;
216252
};
@@ -302,6 +338,14 @@ const buildQueryFilteringArgument = ({ typeName }) =>
302338
})
303339
});
304340

341+
const buildQuerySearchArgument = ({ typeName }) =>
342+
buildInputValue({
343+
name: buildName({ name: SearchArgument.SEARCH }),
344+
type: buildNamedType({
345+
name: `_${typeName}Search`
346+
})
347+
});
348+
305349
/**
306350
* Builds the AST definition for an input object type used
307351
* as the type of a filtering field argument
@@ -324,6 +368,41 @@ export const buildQueryFilteringInputType = ({
324368
return generatedTypeMap;
325369
};
326370

371+
export const buildQuerySearchInputType = ({
372+
typeName,
373+
inputTypeMap,
374+
generatedTypeMap
375+
}) => {
376+
const indexNames = Object.keys(inputTypeMap);
377+
if (indexNames.length) {
378+
// build optional, String type arguments for each search index name
379+
const inputValues = indexNames.map(name =>
380+
buildInputValue({
381+
name: buildName({ name }),
382+
type: buildNamedType({
383+
name: GraphQLString.name
384+
})
385+
})
386+
);
387+
// add a Float type threshold argument used as a>= floor to filter over the score
388+
// statistics for the nodes matched when one of the above search arguments are used
389+
inputValues.push(
390+
buildInputValue({
391+
name: buildName({ name: 'threshold' }),
392+
type: buildNamedType({
393+
name: GraphQLFloat.name
394+
})
395+
})
396+
);
397+
// generate the _${Node}Search input object (overwritten)
398+
generatedTypeMap[typeName] = buildInputObjectType({
399+
name: buildName({ name: typeName }),
400+
fields: inputValues
401+
});
402+
}
403+
return generatedTypeMap;
404+
};
405+
327406
// An enum containing the semantics of logical filtering arguments
328407
const LogicalFilteringArgument = {
329408
AND: 'AND',
@@ -406,6 +485,7 @@ export const buildPropertyFilters = ({
406485
if (!isListFilter) filterTypes = [...filterTypes, 'in', 'not_in'];
407486
filterTypes = [
408487
...filterTypes,
488+
'regexp',
409489
'contains',
410490
'not_contains',
411491
'starts_with',
@@ -446,7 +526,7 @@ export const buildFilters = ({
446526
Neo4jPointDistanceFilter
447527
).some(distanceFilter => distanceFilter === name);
448528
let wrappers = {};
449-
if (name === 'in' || name === 'not_in') {
529+
if ((name === 'in' || name === 'not_in') && name !== 'regexp') {
450530
wrappers = {
451531
[TypeWrappers.NON_NULL_NAMED_TYPE]: true,
452532
[TypeWrappers.LIST_TYPE]: true
@@ -455,10 +535,12 @@ export const buildFilters = ({
455535
fieldConfig.type.name = `${Neo4jTypeName}${SpatialType.POINT}DistanceFilter`;
456536
}
457537
if (isListFilter) {
458-
wrappers = {
459-
[TypeWrappers.NON_NULL_NAMED_TYPE]: true,
460-
[TypeWrappers.LIST_TYPE]: true
461-
};
538+
if (name !== 'regexp') {
539+
wrappers = {
540+
[TypeWrappers.NON_NULL_NAMED_TYPE]: true,
541+
[TypeWrappers.LIST_TYPE]: true
542+
};
543+
}
462544
}
463545
inputValues.push(
464546
buildInputValue({

src/augment/types/node/node.js

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { GraphQLID, GraphQLString } from 'graphql';
12
import {
23
augmentNodeQueryAPI,
34
augmentNodeQueryArgumentTypes,
@@ -14,12 +15,14 @@ import {
1415
import {
1516
FilteringArgument,
1617
OrderingArgument,
17-
augmentInputTypePropertyFields
18+
augmentInputTypePropertyFields,
19+
SearchArgument
1820
} from '../../input-values';
1921
import {
2022
getRelationDirection,
2123
getRelationName,
2224
getDirective,
25+
getDirectiveArgument,
2326
isIgnoredField,
2427
isCypherField,
2528
isPrimaryKeyField,
@@ -58,10 +61,13 @@ export const augmentNodeType = ({
5861
config
5962
}) => {
6063
let nodeInputTypeMap = {};
64+
let searchInputTypeMap = {};
6165
let propertyOutputFields = [];
6266
let propertyInputValues = [];
6367
let extensionPropertyInputValues = [];
6468
let extensionNodeInputTypeMap = {};
69+
// let extensionSearchInputTypeMap = {};
70+
let searchesType = false;
6571
if (isObjectType || isInterfaceType || isUnionType) {
6672
const typeExtensions = typeExtensionDefinitionMap[typeName] || [];
6773
if (typeExtensions.length) {
@@ -78,17 +84,21 @@ export const augmentNodeType = ({
7884
extensionNodeInputTypeMap,
7985
propertyOutputFields,
8086
extensionPropertyInputValues,
81-
isIgnoredType
87+
isIgnoredType,
88+
searchesType,
89+
searchInputTypeMap
8290
] = augmentNodeTypeFields({
8391
typeName,
8492
definition: extension,
8593
typeDefinitionMap,
8694
typeExtensionDefinitionMap,
8795
generatedTypeMap,
96+
searchInputTypeMap,
8897
operationTypeMap,
8998
nodeInputTypeMap: extensionNodeInputTypeMap,
9099
propertyInputValues: extensionPropertyInputValues,
91100
propertyOutputFields,
101+
searchesType,
92102
config
93103
});
94104
if (!isIgnoredType) {
@@ -105,17 +115,21 @@ export const augmentNodeType = ({
105115
nodeInputTypeMap,
106116
propertyOutputFields,
107117
propertyInputValues,
108-
isIgnoredType
118+
isIgnoredType,
119+
searchesType,
120+
searchInputTypeMap
109121
] = augmentNodeTypeFields({
110122
typeName,
111123
definition,
112124
isUnionType,
113125
isQueryType,
126+
searchesType,
114127
typeDefinitionMap,
115128
typeExtensionDefinitionMap,
116129
generatedTypeMap,
117130
operationTypeMap,
118131
nodeInputTypeMap,
132+
searchInputTypeMap,
119133
extensionNodeInputTypeMap,
120134
propertyOutputFields,
121135
propertyInputValues,
@@ -148,10 +162,12 @@ export const augmentNodeType = ({
148162
isUnionType,
149163
isOperationType,
150164
isQueryType,
165+
searchesType,
151166
typeName,
152167
propertyOutputFields,
153168
propertyInputValues,
154169
nodeInputTypeMap,
170+
searchInputTypeMap,
155171
typeDefinitionMap,
156172
typeExtensionDefinitionMap,
157173
generatedTypeMap,
@@ -183,12 +199,14 @@ export const augmentNodeTypeFields = ({
183199
generatedTypeMap,
184200
operationTypeMap,
185201
nodeInputTypeMap = {},
202+
searchInputTypeMap = {},
186203
extensionNodeInputTypeMap,
187204
propertyOutputFields = [],
188205
propertyInputValues = [],
189206
isUnionExtension,
190207
isObjectExtension,
191208
isInterfaceExtension,
209+
searchesType,
192210
config
193211
}) => {
194212
let isIgnoredType = true;
@@ -253,6 +271,25 @@ export const augmentNodeTypeFields = ({
253271
type: unwrappedType,
254272
directives: fieldDirectives
255273
});
274+
if (
275+
outputType === GraphQLID.name ||
276+
outputType === GraphQLString.name
277+
) {
278+
const searchDirective = getDirective({
279+
directives: fieldDirectives,
280+
name: DirectiveDefinition.SEARCH
281+
});
282+
if (searchDirective) {
283+
searchesType = true;
284+
let indexName = getDirectiveArgument({
285+
directive: searchDirective,
286+
name: 'index'
287+
});
288+
// defult search index name for this node type
289+
if (!indexName) indexName = `${typeName}Search`;
290+
searchInputTypeMap[indexName] = true;
291+
}
292+
}
256293
}
257294
} else if (isNodeType({ definition: outputDefinition })) {
258295
[
@@ -341,7 +378,9 @@ export const augmentNodeTypeFields = ({
341378
nodeInputTypeMap,
342379
propertyOutputFields,
343380
propertyInputValues,
344-
isIgnoredType
381+
isIgnoredType,
382+
searchesType,
383+
searchInputTypeMap
345384
];
346385
};
347386

@@ -455,8 +494,10 @@ const augmentNodeTypeAPI = ({
455494
isUnionType,
456495
isOperationType,
457496
isQueryType,
497+
searchesType,
458498
propertyInputValues,
459499
nodeInputTypeMap,
500+
searchInputTypeMap,
460501
typeDefinitionMap,
461502
typeExtensionDefinitionMap,
462503
generatedTypeMap,
@@ -483,8 +524,10 @@ const augmentNodeTypeAPI = ({
483524
isUnionType,
484525
isOperationType,
485526
isQueryType,
527+
searchesType,
486528
propertyInputValues,
487529
nodeInputTypeMap,
530+
searchInputTypeMap,
488531
typeDefinitionMap,
489532
typeExtensionDefinitionMap,
490533
generatedTypeMap,

0 commit comments

Comments
 (0)