Skip to content

Commit

Permalink
feat(hydra-cli): optimized implementation of interface queries (#415)
Browse files Browse the repository at this point in the history
affects: @dzlzv/hydra-cli

feat(hydra-cli): extract orderBy methods in WarthogBaseService

affects: @dzlzv/hydra-cli

feat(hydra-cli): optimized implementation of interfaces fetching

affects: @dzlzv/hydra-cli

fully reworked implementation of the interfaces service with minimal dependency on the code
generation. First, IDs are fetched from a union of each implementation table satisfying the filter,
and, afterward, each table is queried separately by those IDs.

fix(hydra-cli): add TYPEORM_LOGGING option to the graphql-server

affects: @dzlzv/hydra-cli

test(hydra-e2e-test): add e2e tests for interface queries

affects: hydra-e2e-tests

Co-authored-by: metmirr <metmirr@gmail.com>

chore: remove README generation on version

affects: @dzlzv/hydra-cli, @dzlzv/hydra-typegen
  • Loading branch information
dzhelezov committed Jun 9, 2021
1 parent 6f445b0 commit 05c930f
Show file tree
Hide file tree
Showing 29 changed files with 642 additions and 168 deletions.
3 changes: 1 addition & 2 deletions packages/hydra-cli/package.json
Expand Up @@ -39,8 +39,7 @@
"postpack": "rm -f oclif.manifest.json",
"lint": "eslint . --cache --ext .ts",
"prepack": "rm -rf lib && tsc -b && cp -R ./src/templates ./lib/src/templates && oclif-dev manifest",
"test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"",
"version": "oclif-dev readme && git add README.md"
"test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\""
},
"dependencies": {
"@inquirer/input": "^0.0.13-alpha.0",
Expand Down
18 changes: 14 additions & 4 deletions packages/hydra-cli/src/generate/ModelRenderer.ts
Expand Up @@ -54,6 +54,16 @@ export class ModelRenderer extends AbstractRenderer {
}
}

withInterfaceRelationOptions(): GeneratorContext {
if (this.objType.isInterface !== true) {
return {}
}
return {
interfaceRelations: utils.interfaceRelations(this.objType),
interfaceEnumName: `${this.objType.name}TypeOptions`,
}
}

withEnums(): GeneratorContext {
// we need to have a state to render exports only once
const referncedEnums = new Set<GraphQLEnumType>()
Expand All @@ -70,11 +80,10 @@ export class ModelRenderer extends AbstractRenderer {
}

withFields(): GeneratorContext {
const fields: GeneratorContext[] = []
const fields: GeneratorContext[] = this.objType.fields.map((f) =>
buildFieldContext(f, this.objType)
)

utils
.ownFields(this.objType)
.map((f) => fields.push(buildFieldContext(f, this.objType)))
return {
fields,
}
Expand Down Expand Up @@ -210,6 +219,7 @@ export class ModelRenderer extends AbstractRenderer {
...this.withFieldResolvers(),
...utils.withNames(this.objType),
...this.withVariantNames(),
...this.withInterfaceRelationOptions(),
}
}
}
39 changes: 11 additions & 28 deletions packages/hydra-cli/src/generate/field-context.ts
Expand Up @@ -78,15 +78,22 @@ export function buildFieldContext(
): GeneratorContext {
return {
...withFieldTypeGuardProps(f),
...withRequired(f),
...withUnique(f),
...withRelation(f),
...withArrayCustomFieldConfig(f),
...withTsTypeAndDecorator(f),
...withDerivedNames(f, entity),
...withDescription(f),
...withTransformer(f),
...withArrayProp(f),
...withDecoratorOptions(f),
}
}

export function withDecoratorOptions(f: Field): GeneratorContext {
return {
required: !f.nullable,
description: f.description,
unique: f.unique,
array: f.isList,
apiOnly: f.apiOnly,
}
}

Expand All @@ -103,24 +110,6 @@ export function withFieldTypeGuardProps(f: Field): GeneratorContext {
}
}

export function withRequired(f: Field): GeneratorContext {
return {
required: !f.nullable,
}
}

export function withDescription(f: Field): GeneratorContext {
return {
description: f.description,
}
}

export function withUnique(f: Field): GeneratorContext {
return {
unique: f.unique,
}
}

export function withTsTypeAndDecorator(f: Field): GeneratorContext {
const fieldType = f.columnType()
if (TYPE_FIELDS[fieldType]) {
Expand Down Expand Up @@ -182,12 +171,6 @@ export function withRelation(f: Field): GeneratorContext {
}
}

export function withArrayProp(f: Field): GeneratorContext {
return {
array: f.isList,
}
}

export function withTransformer(f: Field): GeneratorContext {
if (
TYPE_FIELDS[f.columnType()] &&
Expand Down
48 changes: 47 additions & 1 deletion packages/hydra-cli/src/generate/utils.ts
Expand Up @@ -2,6 +2,8 @@ import _, { upperFirst, kebabCase, camelCase, snakeCase, toLower } from 'lodash'
import { GeneratorContext } from './SourcesGenerator'
import { ObjectType, Field } from '../model'
import pluralize from 'pluralize'
import { ModelType } from '../model/WarthogModel'
import { GraphQLEnumType, GraphQLEnumValueConfigMap } from 'graphql'

export { upperFirst, kebabCase, camelCase }
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
Expand All @@ -28,6 +30,7 @@ export function names(name: string): { [key: string]: string } {
typeormAliasName: toLower(name), // FIXME: do we have to support other namings?
kebabName: kebabCase(name),
relClassName: pascalCase(name),
aliasName: toLower(name),
relCamelName: camelCase(name),
// Not proper pluralization, but good enough and easy to fix in generated code
camelNamePlural: camelPlural(name),
Expand Down Expand Up @@ -66,6 +69,14 @@ export function ownFields(o: ObjectType): Field[] {
return fields
}

export function interfaceRelations(o: ObjectType): { fieldName: string }[] {
return o.fields
.filter((f) => f.isEntity())
.map((f) => {
return { fieldName: camelCase(f.name) }
})
}

export function generateJoinColumnName(name: string): string {
return snakeCase(name.concat('_id'))
}
Expand Down Expand Up @@ -98,5 +109,40 @@ export function generateResolverReturnType(
* @returns the same string with all whitecharacters removed
*/
export function compact(s: string): string {
return s.replace(/\s/g, '')
return s.replace(/\s+/g, ' ')
}

/**
* Generate EnumField for interface filtering; filter interface by implementers
* e.g where: {type_in: [Type1, Type2]}
*/
export function generateEnumField(typeName: string, apiOnly = true): Field {
const enumField = new Field(`type`, typeName)
enumField.modelType = ModelType.ENUM
enumField.description = 'Filtering options for interface implementers'
enumField.isBuildinType = false
enumField.apiOnly = apiOnly
return enumField
}

export function generateGraphqlEnumType(
name: string,
values: GraphQLEnumValueConfigMap
): GraphQLEnumType {
return new GraphQLEnumType({
name,
values,
})
}

export function generateEnumOptions(
options: string[]
): GraphQLEnumValueConfigMap {
// const values: GraphQLEnumValueConfigMap = this._model
// .getSubclasses(i.name)

return options.reduce((init, option) => {
init[option] = { value: option }
return init
}, {})
}
2 changes: 2 additions & 0 deletions packages/hydra-cli/src/model/Field.ts
Expand Up @@ -33,6 +33,8 @@ export class Field {

derivedFrom?: DerivedFrom

apiOnly?: boolean

constructor(
name: string,
type: string,
Expand Down
1 change: 1 addition & 0 deletions packages/hydra-cli/src/model/ObjectType.ts
Expand Up @@ -12,4 +12,5 @@ export interface ObjectType {
description?: string
isInterface?: boolean
interfaces?: ObjectType[] // interface names
implementers?: string[] // List of interface implementer names
}
3 changes: 2 additions & 1 deletion packages/hydra-cli/src/model/relations.ts
Expand Up @@ -6,6 +6,7 @@ import {
RelationType,
} from '.'
import {
camelCase,
generateJoinColumnName,
generateJoinTableName,
} from '../generate/utils'
Expand Down Expand Up @@ -77,7 +78,7 @@ function addOne2One(rel: EntityRelationship): void {
// Typeorm requires to have ManyToOne field on the related object if the relation is OneToMany
function createAdditionalField(entity: ObjectType, field: Field): Field {
const f = new Field(
entity.name.toLowerCase() + field.name,
camelCase(entity.name.toLowerCase() + field.name),
entity.name,
true,
false,
Expand Down
29 changes: 29 additions & 0 deletions packages/hydra-cli/src/parse/WarthogModelBuilder.ts
Expand Up @@ -18,6 +18,11 @@ import { FTSDirective, FULL_TEXT_SEARCHABLE_DIRECTIVE } from './FTSDirective'
import { availableTypes } from '../model/ScalarTypes'
import * as DerivedFrom from './DerivedFromDirective'
import { RelationshipGenerator } from '../generate/RelationshipGenerator'
import {
generateEnumField,
generateEnumOptions,
generateGraphqlEnumType,
} from '../generate/utils'

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

Expand Down Expand Up @@ -122,6 +127,7 @@ export class WarthogModelBuilder {
isInterface: o.kind === 'InterfaceTypeDefinition',
interfaces:
o.kind === 'ObjectTypeDefinition' ? this.getInterfaces(o) : [],
implementers: [],
} as ObjectType
}

Expand Down Expand Up @@ -180,6 +186,10 @@ export class WarthogModelBuilder {
})
debug(`Read and parsed fields: ${JSON.stringify(fields, null, 2)}`)

if (o.kind === 'InterfaceTypeDefinition') {
fields.push(generateEnumField(`${o.name.value}TypeOptions`))
}

// ---Temporary Solution---
// Warthog's BaseModel already has `id` member so we remove id field from object
// before generation models
Expand Down Expand Up @@ -248,6 +258,23 @@ export class WarthogModelBuilder {
}
}

/**
* It generate enums for interfaces defined in the schema to support type base filtering for
* interface types.
*/
generateEnumsForInterface(): void {
this._model.interfaces.map(({ name }) => {
this._model.addEnum(
generateGraphqlEnumType(
`${name}TypeOptions`,
generateEnumOptions(
this._model.getSubclasses(name).map(({ name }) => name)
)
)
)
})
}

buildWarthogModel(): WarthogModel {
this._model = new WarthogModel()

Expand All @@ -262,6 +289,8 @@ export class WarthogModelBuilder {
DerivedFrom.validateDerivedFields(this._model)
new RelationshipGenerator(this._model).generate()

this.generateEnumsForInterface()

return this._model
}
}
10 changes: 5 additions & 5 deletions packages/hydra-cli/src/templates/entities/model.ts.mst
Expand Up @@ -52,8 +52,7 @@ import { InterfaceType } from 'type-graphql';
{{#isInterface}}
@InterfaceType({{#description}} { description: `{{{description}}}` } {{/description}})
{{/isInterface}}
export {{#isInterface}}abstract{{/isInterface}} class {{className}}
extends {{^interfaces}}BaseModel{{/interfaces}} {{#interfaces}} {{className}} {{/interfaces}} {
export class {{className}} {{^isInterface}}extends BaseModel{{/isInterface}} {

{{#fields}}
{{#is.otm}}
Expand Down Expand Up @@ -144,7 +143,8 @@ export {{#isInterface}}abstract{{/isInterface}} class {{className}}
{{#is.enum}}
@EnumField('{{tsType}}', {{tsType}}, {
{{^required}}nullable: true,{{/required}}
{{#description}}description: `{{{description}}}`{{/description}} })
{{#description}}description: `{{{description}}}`{{/description}}
{{#apiOnly}}, apiOnly: true {{/apiOnly}} })
{{camelName}}{{^required}}?{{/required}}{{#required}}!{{/required}}:{{tsType}}
{{/is.enum}}

Expand All @@ -160,10 +160,10 @@ export {{#isInterface}}abstract{{/isInterface}} class {{className}}

{{/fields}}

{{^interfaces}}
{{^isInterface}}
constructor(init?: Partial<{{className}}>) {
super();
Object.assign(this, init);
}
{{/interfaces}}
{{/isInterface}}
}

0 comments on commit 05c930f

Please sign in to comment.