Commit 0de45aed authored by aleclofabbro's avatar aleclofabbro
Browse files

Merge branch 'feature/queryBuilder'

parents 24a8b8dd 1499b85d
interface Glyph {
_id: ID!
}
input StringMatch {
eq: String
gt: String
lt: String
input StringMatchInput {
_eq: String
_gt: String
_lt: String
}
input PageArg {
input PageInput {
limit: Int
after: ID
}
......@@ -14,77 +14,84 @@ input PageArg {
# Knows
union Knower = User
union Knowable = User
input KnowSubjectQuery {
User: UserQuery
input KnowsSubjectQueryInput {
User: UserQueryInput
}
input KnowObjectQuery {
User: UserQuery
input KnowsObjectQueryInput {
User: UserQueryInput
}
input KnowsQuery {
and: [KnowsQuery!]
or: [KnowsQuery!]
_page: PageArg
input KnowsQueryInput {
_and: [KnowsQueryInput!]
_or: [KnowsQueryInput!]
_id: [ID!]
}
type Knows implements Glyph {
_id: ID!
_subj(query: KnowSubjectQuery): [Knower!]!
_obj(query: KnowObjectQuery): [Knowable!]!
_subj(query: KnowsSubjectQueryInput, page: PageInput): [Knower!]!
_obj(query: KnowsObjectQueryInput, page: PageInput): [Knowable!]!
}
# Follows
union Follower = User
union Followable = User
input FollowObjectQuery {
User: UserQuery
input FollowObjectQueryInput {
User: UserQueryInput
}
input FollowSubjectQuery {
User: UserQuery
input FollowSubjectQueryInput {
User: UserQueryInput
}
input FollowsQuery {
and: [FollowsQuery!]
or: [FollowsQuery!]
_page: PageArg
input FollowsQueryInput {
_and: [FollowsQueryInput!]
_or: [FollowsQueryInput!]
_id: [ID!]
}
type Follows implements Glyph {
_id: ID!
_subj(query: FollowSubjectQuery): [Follower!]!
_obj(query: FollowObjectQuery): [Followable!]!
_subj(query: FollowSubjectQueryInput, page: PageInput): [Follower!]!
_obj(query: FollowObjectQueryInput, page: PageInput): [Followable!]!
}
# User
input UserQuery {
and: [UserQuery!]
or: [UserQuery!]
_id: ID
username: StringMatch
_page: PageArg
input UserQueryInput {
_and: [UserQueryInput!]
_or: [UserQueryInput!]
_id: [ID!]
username: StringMatchInput
}
input UserRelationQuery {
Follows: FollowsQuery
Knows: KnowsQuery
input UserRelationQueryInput {
Follows: FollowsQueryInput
Knows: KnowsQueryInput
}
union UserRelation = Knows | Follows
type User implements Glyph {
_id: ID!
_rel(query: UserRelationQuery): [UserRelation!]!
_rel(query: UserRelationQueryInput, page: PageInput): [UserRelation!]!
username: String!
}
# Main
input GraphQueryInput {
Knows: KnowsQueryInput
Follows: FollowsQueryInput
User: UserQueryInput
}
type Query {
user(query: UserQuery): [User!]!
graph(query: GraphQueryInput, page: PageInput): [Glyph!]!
}
input CreateUserInput {
username: String!
}
type Mutation {
createUser(user: CreateUserInput!): User!
createUser(user: CreateUserInput!): User
createKnows(from: ID!, to: ID!): Knows
createFollows(from: ID!, to: ID!): Follows
}
enum Role {
......
import {
DocumentSelection,
Field,
GraphQuery,
GraphQueryObj,
UnionLookupField,
ValueField,
} from './types'
export const buildQueryDocumentSelection = (
root: GraphQuery | null | undefined
): DocumentSelection | null | undefined => {
return root && buildDocumentSelection(root.qObj)
}
export const buildDocumentSelection = (qObj: GraphQueryObj): DocumentSelection | null => {
if (!(qObj.select.length || qObj.traverse.length)) {
return null
}
const documentValueFieldsSelections = qObj.select.reduce((_documentSelection, selection) => {
const valueField: ValueField = selection
return {
..._documentSelection,
[selection.alias]: valueField,
}
}, {} as DocumentSelection)
const documentUnionLookupFieldSelections = qObj.traverse.reduce(
(_documentSelection, qObjSelection) => {
const _existingAlias = _documentSelection[qObjSelection.alias]
if (_existingAlias && !isUnionLookupField(_existingAlias)) {
throw aliasConflictError(qObjSelection, qObj)
}
const select = buildDocumentSelection(qObjSelection)
if (!select) {
return _documentSelection
}
const unionLookupField: UnionLookupField =
_existingAlias ||
(_documentSelection[qObjSelection.alias] = {
page: qObjSelection.page,
fieldName: qObjSelection.fieldName,
alias: qObjSelection.alias,
lookups: [],
traverseRelation: qObjSelection.traverseRelation,
})
unionLookupField.lookups.push({
__typename: qObjSelection.__typename,
match: qObjSelection.match,
select,
})
return {
..._documentSelection,
[qObjSelection.alias]: unionLookupField,
}
},
{} as DocumentSelection
)
return {
...documentValueFieldsSelections,
...documentUnionLookupFieldSelections,
}
}
const aliasConflictError = (selection: GraphQueryObj, par: GraphQueryObj) => {
console.error(`A ValueField can't have same alias[${selection.alias}] as a UnionLookupField`, par)
return new Error(`A ValueField can't have same alias[${selection.alias}] as a UnionLookupField`)
}
export const isUnionLookupField = (_: Field): _ is UnionLookupField => !!(_ && 'lookups' in _)
......@@ -2,46 +2,55 @@ import { Context } from '../gql'
import { ResolverFn } from '../gql/types'
import { ShallowTypeMocks } from '../gql/shallowTypes'
import { typeInfo, getParent } from './graphQuery'
import { GraphQueryObj } from './types'
import { GraphQueryObj, TraverseRelation } from './types'
export function defaultGraphFieldResolver(): ResolverFn<any, any, Context, any> {
return (parent, args, context, info) => {
const { fieldName, parentType, returnType, schema } = info
if (!context.$graph) {
context.$graph = {
qObj: {
type: 'Query',
query: undefined,
traverse: [],
__typename: 'Query',
alias: 'Query',
directives: {},
traverse: [],
traverseRelation: null,
fieldName: fieldName,
select: [],
},
}
}
const { fieldName, parentType, returnType } = info
const { /* isList, isNullable, */ objs } = typeInfo(returnType)
const __typenames = objs.map((_) => _.name)
const isTop = !info.path.prev
const parentQ = getParent(info, context)
console.log(`defaultFieldResolver `, {
parent,
parentTypeName: parentType.name,
fieldName,
path: info.path,
args,
})
const _typeInfo = typeInfo(schema, returnType)
const __typenames = _typeInfo.objs.map((_) => _.name)
const isTop = !info.path.prev
const parentQ = getParent(info, context)
console.log(`defaultFieldResolver `, {
parentQ,
_typeInfo,
})
if (!parentQ) {
throw new Error('defaultFieldResolver no parentQ')
}
if (isTop || ['_rel', '_subj', '_obj'].includes(fieldName)) {
if (isTop || isTraverseRelation(fieldName)) {
parentQ.traverse.push(
...__typenames.map<GraphQueryObj>((typename) => ({
traverse: [],
select: [],
query: args?.query[typename],
type: typename,
...__typenames.map<GraphQueryObj>((__typename) => ({
__typename,
alias: `${info.path.key}`,
match: args?.query ? args?.query[__typename] : undefined,
page: args?.page,
select: [],
traverse: [],
traverseRelation: isTraverseRelation(fieldName) ? fieldName : null,
directives: {},
fieldName: fieldName,
}))
)
......@@ -57,9 +66,12 @@ export function defaultGraphFieldResolver(): ResolverFn<any, any, Context, any>
return fieldReturn
} else {
if ('string' == typeof info.path.key) {
parentQ.select.push({ field: fieldName, alias: info.path.key })
parentQ.select.push({ fieldName: fieldName, alias: info.path.key })
}
return parent[fieldName]
}
}
}
const isTraverseRelation = (_: string): _ is TraverseRelation =>
['_subj', '_obj', '_rel'].includes(_)
......@@ -8,34 +8,45 @@ import {
GraphQLList,
GraphQLNonNull,
GraphQLUnionType,
GraphQLInterfaceType,
GraphQLSchema,
} from 'graphql'
import { GraphQueryObj } from './types'
export const typeInfo = (
_: GraphQLOutputType,
schema: GraphQLSchema,
gqlOutputType: GraphQLOutputType,
isList = false,
isNullable = true
): { objs: (GraphQLObjectType | GraphQLScalarType)[]; isList: boolean; isNullable: boolean } => {
if (_ instanceof GraphQLObjectType || _ instanceof GraphQLScalarType) {
console.log('gqlOutputType', gqlOutputType)
if (gqlOutputType instanceof GraphQLObjectType || gqlOutputType instanceof GraphQLScalarType) {
return {
objs: [_],
objs: [gqlOutputType],
isList,
isNullable,
}
} else if (_ instanceof GraphQLList) {
return typeInfo(_.ofType, true, isNullable)
} else if (_ instanceof GraphQLNonNull) {
return typeInfo(_.ofType, isList, false)
} else if (_ instanceof GraphQLUnionType) {
const objs = _.getTypes()
} else if (gqlOutputType instanceof GraphQLList) {
return typeInfo(schema, gqlOutputType.ofType, true, isNullable)
} else if (gqlOutputType instanceof GraphQLNonNull) {
return typeInfo(schema, gqlOutputType.ofType, isList, false)
} else if (gqlOutputType instanceof GraphQLUnionType) {
const objs = gqlOutputType.getTypes()
return {
objs,
isList,
isNullable,
}
} else if (gqlOutputType instanceof GraphQLInterfaceType) {
const objs = [...schema.getImplementations(gqlOutputType).objects]
return {
objs,
isList,
isNullable,
}
} else {
console.error(_)
throw new Error(`can't typeInfo on ${_.name}`)
console.error(gqlOutputType)
throw new Error(`can't typeInfo on ${gqlOutputType.name}[${gqlOutputType.constructor.name}]`)
}
}
......
import { DocumentSelection } from '../types'
import { isUnionLookupField } from '../buildDocumentSelection'
export const buildMongoPipeline = (docS: DocumentSelection, notTop?: boolean) => {
const lookups = [] as any[]
const project = { __typename: true } as Record<string, any>
Object.entries(docS).forEach(([_alias, field]) => {
if (isUnionLookupField(field)) {
const unionLookupPipeline = [] as any[]
let _let: any = undefined
const mainLookup = {
as: field.alias,
from: 'Graph',
pipeline: [],
} as any
if (field.traverseRelation) {
if (field.traverseRelation === '_rel') {
_let = { nodeId: '$_id' }
notTop &&
unionLookupPipeline.push({
$match: {
// $expr: { $eq: [`$${field.traverseRelation}`, '$$nodeId'] },
$expr: { $eq: [`$_subj`, '$$nodeId'] },
},
})
} else {
_let = { edgeSide: `$${field.traverseRelation}` } //=== '_obj' ? '$_subj' : '$_obj'
notTop && unionLookupPipeline.push({ $match: { $expr: { $eq: [`$_id`, '$$edgeSide'] } } })
}
}
field.lookups.forEach((fieldLookup, index) => {
const $match = { $and: [{ __typename: fieldLookup.__typename }] as any[] }
fieldLookup.match && $match.$and.push(fieldLookup.match)
if (!index) {
mainLookup.pipeline = [
...unionLookupPipeline,
{ $match },
...buildMongoPipeline(fieldLookup.select, true),
]
_let && (mainLookup.let = _let)
lookups.push({ $lookup: mainLookup })
} else {
mainLookup.pipeline.push({
$unionWith: {
coll: 'Graph',
pipeline: [
...unionLookupPipeline,
{ $match },
...buildMongoPipeline(fieldLookup.select, true) /* [0].$lookup.pipeline */,
],
},
})
}
})
project[field.alias] = true
} else {
project[field.alias] = field.alias === field.fieldName ? true : `$${field.fieldName}`
}
})
if (notTop) {
const stages = [...lookups]
if (Object.keys(project).length) {
stages.push({ $project: project })
}
return stages
} else {
const stages = lookups[0].$lookup.pipeline
return stages
}
}
import { ObjectID } from 'mongodb'
import { GqlNode, GqlRelation, ShallowEntity } from '../types'
import { GqlNode, GqlRelation, ShallowEntity, ShallowRelation } from '../types'
export type MongoNode<T extends GqlNode> = Omit<T, '_id' | '_rel'> & { _id: ObjectID }
export type MongoRelation<T extends GqlRelation> = Omit<T, '_id' | '_subj' | '_obj'> & {
_id: ObjectID
......@@ -7,25 +7,52 @@ export type MongoRelation<T extends GqlRelation> = Omit<T, '_id' | '_subj' | '_o
_subj: ObjectID
}
export const toMongoNode = <N extends GqlNode>(
node: ShallowEntity<N> | Omit<ShallowEntity<N>, '_id'>
): MongoNode<N> => {
const _id = '_id' in node ? new ObjectID(node._id) : new ObjectID()
export const toMongoNode = <Node extends GqlNode>(
gql_node: ShallowEntity<Node> | Omit<ShallowEntity<Node>, '_id'>
): MongoNode<Node> => {
const _id = '_id' in gql_node ? new ObjectID(gql_node._id) : new ObjectID()
const mongo_node = Object.entries(node).reduce(
(_mongo_node, [key, val]) => {
const mongo_node = Object.entries(gql_node).reduce(
(constructing_mongo_node, [key, val]) => {
if (['_id', '_rel'].includes(key)) {
return _mongo_node
return constructing_mongo_node
}
return {
..._mongo_node,
...constructing_mongo_node,
[key]: val,
}
},
{
_id,
} as MongoNode<N>
} as MongoNode<Node>
)
return mongo_node
}
export const toMongoRelation = <Rel extends GqlRelation>(
gql_node: ShallowRelation<Rel> | Omit<ShallowRelation<Rel>, '_id'>,
obj: ObjectID | string,
subj: ObjectID | string
): MongoRelation<Rel> => {
const _id = '_id' in gql_node ? new ObjectID(gql_node._id) : new ObjectID()
const mongo_rel = Object.entries(gql_node).reduce(
(constructing_mongo_rel, [key, val]) => {
if (['_id'].includes(key)) {
return constructing_mongo_rel
}
return {
...constructing_mongo_rel,
[key]: val,
}
},
{
_id,
_obj: new ObjectID(obj),
_subj: new ObjectID(subj),
} as MongoRelation<Rel>
)
return mongo_rel
}
......@@ -7,23 +7,55 @@ export type GqlType = { _id: string; __typename: string }
export type GqlNode = GqlType & { _rel: any[] }
export type GqlRelation = GqlType & { _obj: any; _subj: any }
export type TraverseRelation = '_obj' | '_subj' | '_rel'
// GraphQuery
export type GraphQuery = {
qObj: GraphQueryObj
}
export type GraphQueryObj = {
type: string
query: any
export type GraphQueryObj = Selection & {
__typename: string
traverseRelation: TraverseRelation | null
match?: any
page?: {
limit?: number
after?: string | ObjectID
}
directives: Record<string, any>
select: Selection[]
traverse: GraphQueryObj[]
alias: string
}
export type Selection = {
field: string
fieldName: string
alias: string
}
export type Match = any
export type Limit = any
// Graph DocumentSelection
export type DocumentSelection = {
[alias: string]: Field
}
export type Field = UnionLookupField | ValueField
export type ValueField = Selection
export type UnionLookupField = ValueField & {
page?: {
limit?: number
after?: string | ObjectID
}
traverseRelation: TraverseRelation | null
lookups: FieldLookup[]
}
export type FieldLookup = {
__typename: string
match?: any
select: DocumentSelection
}
......@@ -63,12 +63,12 @@
},
{
"kind": "INPUT_OBJECT",
"name": "StringMatch",
"name": "StringMatchInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "eq",
"name": "_eq",
"description": null,
"type": {
"kind": "SCALAR",
......@@ -78,7 +78,7 @@
"defaultValue": null
},
{
"name": "gt",
"name": "_gt",
"description": null,
"type": {
"kind": "SCALAR",
......@@ -88,7 +88,7 @@
"defaultValue": null
},
{
"name": "lt",
"name": "_lt",
"description": null,
"type": {
"kind": "SCALAR",
......@@ -114,7 +114,7 @@
},
{
"kind": "INPUT_OBJECT",
"name": "PageArg",
"name": "PageInput",
"description": null,
"fields": null,
"inputFields": [
......@@ -187,7 +187,7 @@