Skip to content

Commit

Permalink
feat(hydra-cli): support cross filters for entity relationship (Joyst…
Browse files Browse the repository at this point in the history
…ream#381)

* hydra-cli: update relation decorator with new options

* hydra-cli: cross filter queries for oto, mto, otm relation types

* hydra-cli: update templates for MTM relations

* hydra-cli: fix table name

* hydra-cli: cross-relation filters docs

* hydra-cli: fix tests

* hydra-cli: upgrade warthog version
  • Loading branch information
metmirr authored and dzhelezov committed May 7, 2021
1 parent c65ac37 commit 04f2dca
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 37 deletions.
111 changes: 111 additions & 0 deletions docs/schema-spec/cross-filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
description: Filter query results with related entity fields
---

# Cross-relation filters

Cross-relation filters allows you to filter query results with the related entity fields.

During the example we will use the below schema:

```graphql
type Channel @entity {
id: ID!
handle: String!
videos: [Video!] @derivedFrom(field: "channel")
}

type Video @entity {
id: ID!
title: String!
channel: Channel!
featured: FeaturedVideo @derivedFrom(field: "video")
publishedBefore: Bool!
}

type FeaturedVideo @entity {
id: ID!
video: Video!
}
```

**Filter 1-1 relations**

Fetch all the featured videos those title contains `joy`:

```graphql
query {
featuredVideos(where: { video: { title_contains: "joy" } }) {
id
video {
title
}
}
}
```

**Filter 1-M relations**

Fetch all the videos published under `Joystream` channel:

```graphql
query {
videos(where: { channel: { handle_eq: "Joystream" } }) {
title
}
}
```

**Modifiers**
There are three modifiers can be use for M-1 and M-M relationships.

- some: if any of the entities in the relation satify a condition
- every: if all of the entities in the relation satify a condition
- none: if none of the entities in the relation satify a condition

**Fetch if any of the entities in the relation satify a condition**
Example:

Fetch all channels which have at least one video with a title that contains `kid`

```graphql
query {
channels(where: { videos_some: { title_contains: "kid" } }) {
handle
videos {
title
}
}
}
```

**Fetch if all of the entities in the relation satify a condition**
Example:

Fetch all channels which have all of their videos `publishedBefore_eq: true`:

```graphql
query {
channels(where: { videos_every: { publishedBefore_eq: true } }) {
handle
videos {
title
}
}
}
```

**Fetch if none of the entities in the relation satify a condition**

Fetch all channels which have none of their videos `publishedBefore_eq: true`:

```graphql
query {
channels(where: { videos_none: { publishedBefore_eq: true } }) {
handle
videos {
title
}
}
}
```
4 changes: 2 additions & 2 deletions packages/hydra-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@
"mustache": "^4.0.1",
"pluralize": "^8.0.0",
"tslib": "1.11.2",
"warthog": "/~https://github.com/metmirr/warthog/releases/download/v2.23.0/warthog-v2.23.0.tgz"
"warthog": "/~https://github.com/metmirr/warthog/releases/download/v2.26.0/warthog-v2.26.0.tgz"
},
"queryNodeDependencies": {
"lodash": "^4.17.20",
"pg-listen": "^1.7.0",
"warthog": "/~https://github.com/metmirr/warthog/releases/download/v2.23.0/warthog-v2.23.0.tgz",
"warthog": "/~https://github.com/metmirr/warthog/releases/download/v2.26.0/warthog-v2.26.0.tgz",
"typeorm": "^0.2.31"
},
"devDependencies": {
Expand Down
15 changes: 14 additions & 1 deletion packages/hydra-cli/src/generate/ModelRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as utils from './utils'
import { GraphQLEnumType } from 'graphql'
import { AbstractRenderer } from './AbstractRenderer'
import { withEnum } from './enum-context'
import { getRelationType } from '../model/Relation'

const debug = Debug('qnode-cli:model-renderer')

Expand Down Expand Up @@ -134,26 +135,38 @@ export class ModelRenderer extends AbstractRenderer {
for (const f of this.objType.fields) {
if (!f.relation) continue
const returnTypeFunc = f.relation.columnType

fieldResolvers.push({
returnTypeFunc,
rootArgType: entityName,
fieldName: f.name,
rootArgName: 'r', // disable utils.camelCase(entityName) could be a reverved ts/js keyword ie `class`
returnType: utils.generateResolverReturnType(returnTypeFunc, f.isList),
relatedTsProp: f.relation.relatedTsProp,
relationType: getRelationType(f.relation),
tableName: returnTypeFunc.toLowerCase(),
})
if (f.type !== this.objType.name) {
fieldResolverImports.add(utils.generateEntityImport(returnTypeFunc))
fieldResolverImports.add(
utils.generateEntityServiceImport(returnTypeFunc)
)
}
}
const imports = Array.from(fieldResolverImports.values())
// If there is at least one field resolver then add typeorm to imports
if (imports.length) {
imports.push(`import { getConnection } from 'typeorm';`)
imports.push(
`import { getConnection, getRepository, In, Not } from 'typeorm';
import _ from 'lodash';
`
)
}

return {
fieldResolvers,
fieldResolverImports: imports,
crossFilters: !!fieldResolvers.length,
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/hydra-cli/src/generate/field-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export function withDerivedNames(
...util.names(f.name),
relFieldName: util.camelCase(entity.name),
relFieldNamePlural: util.camelPlural(entity.name),
entityName: entity.name,
}
}

Expand Down
5 changes: 5 additions & 0 deletions packages/hydra-cli/src/generate/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export function generateEntityImport(entityName: string): string {
return `import {${entityName}} from '../${kebabName}/${kebabName}.model'`
}

export function generateEntityServiceImport(name: string): string {
const kebabName = kebabCase(name)
return `import {${name}Service} from '../${kebabName}/${kebabName}.service'`
}

export function generateResolverReturnType(
type: string,
isList: boolean
Expand Down
20 changes: 20 additions & 0 deletions packages/hydra-cli/src/model/Relation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export interface FieldResolver {
rootArgName: string
rootArgType: string
returnType: string
relatedTsProp: string | undefined
relationType: RelationTypeGuard
tableName: string
}

export interface EntityRelationship {
Expand Down Expand Up @@ -61,3 +64,20 @@ export enum RelationType {
// ManyToOne
MTO = 'mto',
}
interface RelationTypeGuard {
isOTO: boolean
isOTM: boolean
isMTO: boolean
isMTM: boolean
isModifier: boolean
}

export function getRelationType(r: Relation): RelationTypeGuard {
return {
isOTO: r.type === RelationType.OTO,
isOTM: r.type === RelationType.OTM,
isMTO: r.type === RelationType.MTO,
isMTM: r.type === RelationType.MTM,
isModifier: r.type === RelationType.OTM || r.type === RelationType.MTM,
}
}
39 changes: 33 additions & 6 deletions packages/hydra-cli/src/templates/entities/model.ts.mst
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,13 @@ export {{#isInterface}}abstract{{/isInterface}} class {{className}}
{{#fields}}
{{#is.otm}}
@OneToMany(
() => {{relation.columnType}}, (param: {{relation.columnType}}) => param.{{relation.relatedTsProp}}
{{#relation.nullable}},{ nullable: true }{{/relation.nullable}}
() => {{relation.columnType}}, (param: {{relation.columnType}}) => param.{{relation.relatedTsProp}},
{
{{#relation.nullable}}nullable: true,{{/relation.nullable}}
modelName: '{{entityName}}',
relModelName: '{{relation.columnType}}',
propertyName: '{{camelName}}'
}
)
{{camelName}}?: {{relation.columnType}}[];
{{/is.otm}}
Expand All @@ -68,21 +73,43 @@ export {{#isInterface}}abstract{{/isInterface}} class {{className}}
@ManyToOne(
() => {{relation.columnType}},
{{#relation.relatedTsProp}}(param: {{relation.columnType}}) => param.{{relation.relatedTsProp}},{{/relation.relatedTsProp}}
{ skipGraphQLField: true {{#relation.nullable}},nullable: true{{/relation.nullable}} }
{
skipGraphQLField: true,
{{#relation.nullable}}nullable: true,{{/relation.nullable}}
modelName: '{{entityName}}',
relModelName: '{{relation.columnType}}',
propertyName: '{{camelName}}'
}
)
{{camelName}}{{^required}}?{{/required}}{{#required}}!{{/required}}: {{relation.columnType}};
{{/is.mto}}

{{#is.oto}}
{{^relation.joinColumn}}@OneToOne{{/relation.joinColumn}}
{{#relation.joinColumn}}@OneToOneJoin{{/relation.joinColumn}}
(() => {{relation.columnType}},(param: {{relation.columnType}}) => param.{{relation.relatedTsProp}} {{#relation.nullable}},{ nullable: true }{{/relation.nullable}} )
(
() => {{relation.columnType}},
(param: {{relation.columnType}}) => param.{{relation.relatedTsProp}},
{
{{#relation.nullable}}nullable: true,{{/relation.nullable}}
modelName: '{{entityName}}',
relModelName: '{{relation.columnType}}',
propertyName: '{{camelName}}',
}
)
{{camelName}}{{^required}}?{{/required}}{{#required}}!{{/required}}: {{relation.columnType}};
{{/is.oto}}

{{#is.mtm}}
@ManyToMany(() => {{relation.columnType}}, (param: {{relation.columnType}}) => param.{{relation.relatedTsProp}}
{{#relation.nullable}},{ nullable: true }{{/relation.nullable}}
@ManyToMany(
() => {{relation.columnType}},
(param: {{relation.columnType}}) => param.{{relation.relatedTsProp}},
{
{{#relation.nullable}},{ nullable: true }{{/relation.nullable}}
modelName: '{{entityName}}',
relModelName: '{{relation.columnType}}',
propertyName: '{{camelName}}',
}
)
{{#relation.joinTable}}
@JoinTable({
Expand Down
Loading

0 comments on commit 04f2dca

Please sign in to comment.